feat(bridge): count the twenty-three chat-title conditions in player.tally (PLAN_REDESIGNS §5) #20
@@ -245,6 +245,9 @@ namespace Oxide.Plugins
|
|||||||
"OnClanMemberLeft", "OnClanMemberKicked", "OnClanColorChanged",
|
"OnClanMemberLeft", "OnClanMemberKicked", "OnClanColorChanged",
|
||||||
"OnKitRedeemed",
|
"OnKitRedeemed",
|
||||||
"OnEntitySpawned", "OnEntityKill",
|
"OnEntitySpawned", "OnEntityKill",
|
||||||
|
"OnEntityTakeDamage", "OnItemCraftFinished", "OnEntityBuilt", "OnStructureRepair",
|
||||||
|
"OnHealingItemUse", "OnPlayerRevive", "OnRocketLaunched",
|
||||||
|
"OnExplosiveThrown", "OnExplosiveDropped", "OnMissionSucceeded",
|
||||||
"OnEnterZone", "OnExitZone", "OnRgDomesReady"
|
"OnEnterZone", "OnExitZone", "OnRgDomesReady"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1314,15 +1317,7 @@ namespace Oxide.Plugins
|
|||||||
BaseEntity initiator = info != null ? info.Initiator : null;
|
BaseEntity initiator = info != null ? info.Initiator : null;
|
||||||
BasePlayer attacker = info != null ? info.InitiatorPlayer : null;
|
BasePlayer attacker = info != null ? info.InitiatorPlayer : null;
|
||||||
|
|
||||||
string weapon = null;
|
string weapon = WeaponOf(info);
|
||||||
|
|
||||||
if (info != null)
|
|
||||||
{
|
|
||||||
if (info.WeaponPrefab != null)
|
|
||||||
weapon = info.WeaponPrefab.ShortPrefabName;
|
|
||||||
else if (info.Weapon != null)
|
|
||||||
weapon = info.Weapon.ShortPrefabName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (weapon != null)
|
if (weapon != null)
|
||||||
frame["weapon"] = weapon;
|
frame["weapon"] = weapon;
|
||||||
@@ -1351,16 +1346,49 @@ namespace Oxide.Plugins
|
|||||||
frame["attackerName"] = initiator.ShortPrefabName;
|
frame["attackerName"] = initiator.ShortPrefabName;
|
||||||
}
|
}
|
||||||
|
|
||||||
float distance = info != null ? info.ProjectileDistance : 0f;
|
float distance = DistanceOf(info, victim);
|
||||||
|
|
||||||
if (distance <= 0f && initiator.transform != null && victim.transform != null)
|
|
||||||
distance = UnityEngine.Vector3.Distance(
|
|
||||||
initiator.transform.position, victim.transform.position);
|
|
||||||
|
|
||||||
if (distance > 0f)
|
if (distance > 0f)
|
||||||
frame["distance"] = (float)Math.Round(distance, 1);
|
frame["distance"] = (float)Math.Round(distance, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The killing blow's weapon as the killfeed names it: the held entity's short prefab
|
||||||
|
/// name (<c>bow_hunting.entity</c>), or what was thrown or placed. Null when the hit
|
||||||
|
/// carries neither. The tally's weapon kills (§5.3) read this same name, so the lists
|
||||||
|
/// the site keeps are lists of what the killfeed already shows.
|
||||||
|
/// </summary>
|
||||||
|
private static string WeaponOf(HitInfo info)
|
||||||
|
{
|
||||||
|
if (info == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (info.WeaponPrefab != null)
|
||||||
|
return info.WeaponPrefab.ShortPrefabName;
|
||||||
|
|
||||||
|
return info.Weapon != null ? info.Weapon.ShortPrefabName : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How far the killing blow travelled, in metres: the projectile's own distance, or the
|
||||||
|
/// gap between the two when there was no projectile. 0 when neither can be read.
|
||||||
|
/// </summary>
|
||||||
|
private static float DistanceOf(HitInfo info, BaseEntity victim)
|
||||||
|
{
|
||||||
|
if (info == null)
|
||||||
|
return 0f;
|
||||||
|
|
||||||
|
float distance = info.ProjectileDistance;
|
||||||
|
BaseEntity initiator = info.Initiator;
|
||||||
|
|
||||||
|
if (distance <= 0f && initiator != null && initiator.transform != null
|
||||||
|
&& victim != null && victim.transform != null)
|
||||||
|
distance = UnityEngine.Vector3.Distance(
|
||||||
|
initiator.transform.position, victim.transform.position);
|
||||||
|
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- the map grid ----
|
// ---- the map grid ----
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1643,8 +1671,14 @@ namespace Oxide.Plugins
|
|||||||
BasePlayer killer = info != null ? info.InitiatorPlayer : null;
|
BasePlayer killer = info != null ? info.InitiatorPlayer : null;
|
||||||
|
|
||||||
if (killer != null && !killer.IsNpc && (ulong)killer.userID != (ulong)player.userID)
|
if (killer != null && !killer.IsNpc && (ulong)killer.userID != (ulong)player.userID)
|
||||||
|
{
|
||||||
CreditKill(killer, true);
|
CreditKill(killer, true);
|
||||||
|
|
||||||
|
// The kill itself is `player.death`'s, counted by the site from this frame; the
|
||||||
|
// tally adds only what a title needs about it (§5.1).
|
||||||
|
TallyFor(killer).Killed(info, player, true);
|
||||||
|
}
|
||||||
|
|
||||||
Dictionary<string, object> frame = Frame("player.death", "event");
|
Dictionary<string, object> frame = Frame("player.death", "event");
|
||||||
frame["steamId"] = SteamIdOf(player);
|
frame["steamId"] = SteamIdOf(player);
|
||||||
frame["name"] = player.displayName;
|
frame["name"] = player.displayName;
|
||||||
@@ -1767,6 +1801,15 @@ namespace Oxide.Plugins
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
BasePlayer attacker = info != null ? info.InitiatorPlayer : null;
|
BasePlayer attacker = info != null ? info.InitiatorPlayer : null;
|
||||||
|
|
||||||
|
// Before the early return below: a patrol helicopter usually dies by crashing, and a
|
||||||
|
// crash's HitInfo names no player (§5.4).
|
||||||
|
if (IsVehicleKill(entity))
|
||||||
|
{
|
||||||
|
CreditVehicle(entity, attacker);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
bool byPlayer = attacker != null && !attacker.IsNpc;
|
bool byPlayer = attacker != null && !attacker.IsNpc;
|
||||||
|
|
||||||
// Nothing below is interesting unless a real player did it. That single test discards
|
// Nothing below is interesting unless a real player did it. That single test discards
|
||||||
@@ -1782,7 +1825,9 @@ namespace Oxide.Plugins
|
|||||||
// double every kill in the feed. Only the NPC kind is ours to count.
|
// double every kill in the feed. Only the NPC kind is ours to count.
|
||||||
if (victimPlayer.IsNpc)
|
if (victimPlayer.IsNpc)
|
||||||
{
|
{
|
||||||
TallyFor(attacker).NpcKills++;
|
Tally tally = TallyFor(attacker);
|
||||||
|
tally.NpcKills++;
|
||||||
|
tally.Killed(info, entity, false);
|
||||||
CreditKill(attacker, false);
|
CreditKill(attacker, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1791,9 +1836,14 @@ namespace Oxide.Plugins
|
|||||||
|
|
||||||
// An animal is an NPC to a run's tally exactly as it is to the leaderboard (§29.5):
|
// An animal is an NPC to a run's tally exactly as it is to the leaderboard (§29.5):
|
||||||
// two meanings of one word would be two answers to "how many NPCs did I kill".
|
// two meanings of one word would be two answers to "how many NPCs did I kill".
|
||||||
|
// `animalKills` counts it AGAIN, separately (§5.2): the public NPC column keeps its
|
||||||
|
// meaning mid-wipe, and the site takes the human NPCs as the difference.
|
||||||
if (entity is BaseNpc)
|
if (entity is BaseNpc)
|
||||||
{
|
{
|
||||||
TallyFor(attacker).NpcKills++;
|
Tally tally = TallyFor(attacker);
|
||||||
|
tally.NpcKills++;
|
||||||
|
tally.AnimalKills++;
|
||||||
|
tally.Killed(info, entity, false);
|
||||||
CreditKill(attacker, false);
|
CreditKill(attacker, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1841,6 +1891,258 @@ namespace Oxide.Plugins
|
|||||||
EmitFrame(frame);
|
EmitFrame(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- what a chat title counts (PLAN_REDESIGNS §5, D172–D174) ----
|
||||||
|
//
|
||||||
|
// Every one of these is a counter (rule 2) and void (rule 1). Three of them can veto —
|
||||||
|
// `OnStructureRepair`, `OnHealingItemUse`, `OnPlayerRevive` — and `OnEntityTakeDamage`
|
||||||
|
// can cancel the damage outright, which is exactly why none of them returns anything.
|
||||||
|
|
||||||
|
/// <summary>How long a player's hit on a vehicle still earns its kill when the blow that ends it names nobody (§5.4).</summary>
|
||||||
|
private const float VehicleCreditSeconds = 60f;
|
||||||
|
|
||||||
|
/// <summary>The last player to hurt each APC or helicopter, by net id. A handful at most.</summary>
|
||||||
|
private readonly Dictionary<ulong, KeyValuePair<ulong, float>> _vehicleHits =
|
||||||
|
new Dictionary<ulong, KeyValuePair<ulong, float>>();
|
||||||
|
|
||||||
|
/// <summary>The APC and the two helicopters: the three things a title credits a kill of (D174).</summary>
|
||||||
|
private static bool IsVehicleKill(BaseEntity entity)
|
||||||
|
{
|
||||||
|
return entity is BradleyAPC || entity is PatrolHelicopter || entity is CH47Helicopter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Void: a non-null return cancels the damage. It fires for every hit on everything,
|
||||||
|
/// so the type test is the first thing it does; only a hit on one of the three
|
||||||
|
/// vehicles is remembered, and only who dealt it and when.
|
||||||
|
/// </summary>
|
||||||
|
private void OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
|
||||||
|
{
|
||||||
|
MarkHook("OnEntityTakeDamage");
|
||||||
|
|
||||||
|
if (entity == null || info == null || !IsVehicleKill(entity) || entity.net == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
BasePlayer player = info.InitiatorPlayer;
|
||||||
|
if (player == null || player.IsNpc)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float now = UnityEngine.Time.realtimeSinceStartup;
|
||||||
|
|
||||||
|
// A helicopter that flies off the map is never killed, so its row is never removed
|
||||||
|
// at death; a stale one is dropped here instead.
|
||||||
|
if (_vehicleHits.Count > 16)
|
||||||
|
{
|
||||||
|
var stale = new List<ulong>();
|
||||||
|
foreach (KeyValuePair<ulong, KeyValuePair<ulong, float>> hit in _vehicleHits)
|
||||||
|
if (now - hit.Value.Value > VehicleCreditSeconds)
|
||||||
|
stale.Add(hit.Key);
|
||||||
|
foreach (ulong id in stale)
|
||||||
|
_vehicleHits.Remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
_vehicleHits[entity.net.ID.Value] = new KeyValuePair<ulong, float>(player.userID, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The killing blow's player (D174), or, when the blow names nobody — a helicopter
|
||||||
|
/// crashing — the last player who hurt it within <see cref="VehicleCreditSeconds"/>.
|
||||||
|
/// </summary>
|
||||||
|
private void CreditVehicle(BaseCombatEntity entity, BasePlayer attacker)
|
||||||
|
{
|
||||||
|
KeyValuePair<ulong, float> last = default(KeyValuePair<ulong, float>);
|
||||||
|
bool hit = entity.net != null && _vehicleHits.TryGetValue(entity.net.ID.Value, out last);
|
||||||
|
|
||||||
|
if (entity.net != null)
|
||||||
|
_vehicleHits.Remove(entity.net.ID.Value);
|
||||||
|
|
||||||
|
BasePlayer credited = attacker != null && !attacker.IsNpc ? attacker : null;
|
||||||
|
|
||||||
|
if (credited == null && hit && UnityEngine.Time.realtimeSinceStartup - last.Value <= VehicleCreditSeconds)
|
||||||
|
credited = BasePlayer.FindByID(last.Key) ?? BasePlayer.FindSleeping(last.Key);
|
||||||
|
|
||||||
|
if (credited == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Tally tally = TallyFor(credited);
|
||||||
|
|
||||||
|
if (entity is BradleyAPC)
|
||||||
|
tally.ApcKills++;
|
||||||
|
else
|
||||||
|
tally.HeliKills++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No return behaviour. A crafted shirt, rifle or anything else, by its item category.
|
||||||
|
/// Who crafted it is the crafter's owner: the task no longer carries one (read off the
|
||||||
|
/// rig, 2026-09-29).
|
||||||
|
/// </summary>
|
||||||
|
private void OnItemCraftFinished(ItemCraftTask task, Item item, ItemCrafter crafter)
|
||||||
|
{
|
||||||
|
MarkHook("OnItemCraftFinished");
|
||||||
|
|
||||||
|
BasePlayer player = crafter != null ? crafter.owner : null;
|
||||||
|
|
||||||
|
if (player == null || player.IsNpc || item == null || item.info == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TallyFor(player).Craft(item.info.category.ToString().ToLowerInvariant(), item.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>No return behaviour. Only a building block is a structure built; a deployable is not.</summary>
|
||||||
|
private void OnEntityBuilt(Planner planner, UnityEngine.GameObject go)
|
||||||
|
{
|
||||||
|
MarkHook("OnEntityBuilt");
|
||||||
|
|
||||||
|
BasePlayer player = planner != null ? planner.GetOwnerPlayer() : null;
|
||||||
|
|
||||||
|
if (player == null || player.IsNpc || go == null || go.GetComponent<BuildingBlock>() == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TallyFor(player).Built++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Void: a non-null return refuses the repair. It fires on every hammer hit, BEFORE the
|
||||||
|
/// game decides whether there is anything to repair, the resources to pay for it, or a
|
||||||
|
/// recent attack that forbids it. So the health is read now and again next tick, and
|
||||||
|
/// only a hit that actually raised it is a repair.
|
||||||
|
/// </summary>
|
||||||
|
private void OnStructureRepair(BaseCombatEntity entity, BasePlayer player)
|
||||||
|
{
|
||||||
|
MarkHook("OnStructureRepair");
|
||||||
|
|
||||||
|
if (entity == null || player == null || player.IsNpc)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float before = entity.health;
|
||||||
|
ulong id = player.userID;
|
||||||
|
|
||||||
|
NextTick(() =>
|
||||||
|
{
|
||||||
|
if (entity == null || entity.IsDestroyed || entity.health <= before)
|
||||||
|
return;
|
||||||
|
|
||||||
|
BasePlayer still = BasePlayer.FindByID(id);
|
||||||
|
if (still != null)
|
||||||
|
TallyFor(still).Repaired++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// A syringe on a downed player raises `OnPlayerRevive` and then `OnHealingItemUse` for the
|
||||||
|
// same use; one act of help is one heal. The revive is remembered for the frame it happened in.
|
||||||
|
private ulong _reviveHealer;
|
||||||
|
private ulong _reviveTarget;
|
||||||
|
private int _reviveFrame = -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Void: a non-null return refuses the heal. <paramref name="target"/> is what is healed;
|
||||||
|
/// the tool's owner is who heals. Healing yourself is not a heal (D174).
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// The target is taken as <c>object</c>: the game's `GiveEffectsTo` now takes an
|
||||||
|
/// `IMedicalToolTarget`, which is not always a player, and a parameter typed
|
||||||
|
/// `BasePlayer` would simply never be called for the hook's current shape. Only a
|
||||||
|
/// player healed counts.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
private void OnHealingItemUse(MedicalTool tool, object target)
|
||||||
|
{
|
||||||
|
MarkHook("OnHealingItemUse");
|
||||||
|
|
||||||
|
BasePlayer player = target as BasePlayer;
|
||||||
|
BasePlayer healer = tool != null ? tool.GetOwnerPlayer() : null;
|
||||||
|
|
||||||
|
if (healer == null || healer.IsNpc || player == null || player.IsNpc
|
||||||
|
|| (ulong)healer.userID == (ulong)player.userID)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_reviveFrame == UnityEngine.Time.frameCount
|
||||||
|
&& _reviveHealer == (ulong)healer.userID && _reviveTarget == (ulong)player.userID)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TallyFor(healer).Healed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Void: a non-null return refuses the revive. Reviving yourself is not possible, and is not counted.</summary>
|
||||||
|
private void OnPlayerRevive(BasePlayer reviver, BasePlayer player)
|
||||||
|
{
|
||||||
|
MarkHook("OnPlayerRevive");
|
||||||
|
|
||||||
|
if (reviver == null || reviver.IsNpc || player == null || player.IsNpc
|
||||||
|
|| (ulong)reviver.userID == (ulong)player.userID)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_reviveHealer = reviver.userID;
|
||||||
|
_reviveTarget = player.userID;
|
||||||
|
_reviveFrame = UnityEngine.Time.frameCount;
|
||||||
|
|
||||||
|
TallyFor(reviver).Healed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>No return behaviour. One rocket leaving a launcher.</summary>
|
||||||
|
private void OnRocketLaunched(BasePlayer player, BaseEntity entity)
|
||||||
|
{
|
||||||
|
MarkHook("OnRocketLaunched");
|
||||||
|
|
||||||
|
if (player == null || player.IsNpc)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TallyFor(player).Rockets++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>No return behaviour. A grenade, beancan, satchel or C4 thrown.</summary>
|
||||||
|
private void OnExplosiveThrown(BasePlayer player, BaseEntity entity, ThrownWeapon item)
|
||||||
|
{
|
||||||
|
MarkHook("OnExplosiveThrown");
|
||||||
|
CountExplosive(player, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>No return behaviour. The same, dropped at your feet rather than thrown.</summary>
|
||||||
|
private void OnExplosiveDropped(BasePlayer player, BaseEntity entity, ThrownWeapon item)
|
||||||
|
{
|
||||||
|
MarkHook("OnExplosiveDropped");
|
||||||
|
CountExplosive(player, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A supply signal and a smoke grenade are thrown through the same hooks, and neither
|
||||||
|
/// is an explosive. What is one is a timed explosive that does damage.
|
||||||
|
/// </summary>
|
||||||
|
private void CountExplosive(BasePlayer player, BaseEntity entity)
|
||||||
|
{
|
||||||
|
if (player == null || player.IsNpc)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TimedExplosive explosive = entity as TimedExplosive;
|
||||||
|
if (explosive == null || explosive is SupplySignal || explosive.damageTypes == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (Rust.DamageTypeEntry damage in explosive.damageTypes)
|
||||||
|
{
|
||||||
|
if (damage != null && damage.amount > 0f)
|
||||||
|
{
|
||||||
|
TallyFor(player).Explosives++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No return behaviour. A mission one of the game's NPCs handed out, finished (D173). A
|
||||||
|
/// failed or abandoned one raises a different hook, and is never counted.
|
||||||
|
/// </summary>
|
||||||
|
private void OnMissionSucceeded(BaseMission mission, BaseMission.MissionInstance instance, BasePlayer assignee)
|
||||||
|
{
|
||||||
|
MarkHook("OnMissionSucceeded");
|
||||||
|
|
||||||
|
// The game hands the assignee over: `MissionSuccess(instance, assignee)`. The instance
|
||||||
|
// itself has no accessor for it (read off the rig, 2026-09-29).
|
||||||
|
if (assignee == null || assignee.IsNpc)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TallyFor(assignee).Missions++;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The most people one <c>entity.destroyed</c> frame names as authorised. A cupboard
|
/// The most people one <c>entity.destroyed</c> frame names as authorised. A cupboard
|
||||||
/// has no hard cap on vanilla, and an unbounded list is an unbounded line on the game
|
/// has no hard cap on vanilla, and an unbounded list is an unbounded line on the game
|
||||||
@@ -2462,19 +2764,70 @@ namespace Oxide.Plugins
|
|||||||
public int Structures;
|
public int Structures;
|
||||||
public string Name;
|
public string Name;
|
||||||
|
|
||||||
|
// Protocol 13, the chat-title conditions (PLAN_REDESIGNS §5.1). Sums unless named a best.
|
||||||
|
public int AnimalKills;
|
||||||
|
public readonly Dictionary<string, int> WeaponKills = new Dictionary<string, int>();
|
||||||
|
public int Headshots;
|
||||||
|
/// <summary>The longest player kill this interval, in metres: a best, not a sum.</summary>
|
||||||
|
public float BestPvpM;
|
||||||
|
/// <summary>The longest NPC or animal kill this interval: a best, not a sum.</summary>
|
||||||
|
public float BestPveM;
|
||||||
|
public int ApcKills;
|
||||||
|
public int HeliKills;
|
||||||
|
public readonly Dictionary<string, int> Crafted = new Dictionary<string, int>();
|
||||||
|
public int Built;
|
||||||
|
public int Repaired;
|
||||||
|
public int Healed;
|
||||||
|
public int Rockets;
|
||||||
|
public int Explosives;
|
||||||
|
public int Missions;
|
||||||
|
|
||||||
public void Gather(string shortname, int amount)
|
public void Gather(string shortname, int amount)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(shortname) || amount <= 0)
|
Add(Gathered, shortname, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Craft(string category, int amount)
|
||||||
|
{
|
||||||
|
Add(Crafted, category, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What a credited kill adds for the titles: its weapon (any victim, D174), a
|
||||||
|
/// headshot, and its distance as this interval's best of its kind.
|
||||||
|
/// </summary>
|
||||||
|
public void Killed(HitInfo info, BaseEntity victim, bool victimIsPlayer)
|
||||||
|
{
|
||||||
|
Add(WeaponKills, WeaponOf(info), 1);
|
||||||
|
|
||||||
|
if (info != null && info.isHeadshot)
|
||||||
|
Headshots++;
|
||||||
|
|
||||||
|
float distance = DistanceOf(info, victim);
|
||||||
|
|
||||||
|
if (victimIsPlayer)
|
||||||
|
BestPvpM = Math.Max(BestPvpM, distance);
|
||||||
|
else
|
||||||
|
BestPveM = Math.Max(BestPveM, distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Add(Dictionary<string, int> into, string key, int amount)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key) || amount <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int have;
|
int have;
|
||||||
Gathered.TryGetValue(shortname, out have);
|
into.TryGetValue(key, out have);
|
||||||
Gathered[shortname] = have + amount;
|
into[key] = have + amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsEmpty()
|
public bool IsEmpty()
|
||||||
{
|
{
|
||||||
return Gathered.Count == 0 && NpcKills == 0 && Structures == 0;
|
return Gathered.Count == 0 && NpcKills == 0 && Structures == 0
|
||||||
|
&& AnimalKills == 0 && WeaponKills.Count == 0 && Headshots == 0
|
||||||
|
&& BestPvpM <= 0f && BestPveM <= 0f && ApcKills == 0 && HeliKills == 0
|
||||||
|
&& Crafted.Count == 0 && Built == 0 && Repaired == 0 && Healed == 0
|
||||||
|
&& Rockets == 0 && Explosives == 0 && Missions == 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2544,6 +2897,36 @@ namespace Oxide.Plugins
|
|||||||
if (tally.Structures > 0)
|
if (tally.Structures > 0)
|
||||||
frame["structures"] = tally.Structures;
|
frame["structures"] = tally.Structures;
|
||||||
|
|
||||||
|
// The title conditions (§5.1), each absent when nothing moved, as the three above.
|
||||||
|
if (tally.AnimalKills > 0)
|
||||||
|
frame["animalKills"] = tally.AnimalKills;
|
||||||
|
if (tally.WeaponKills.Count > 0)
|
||||||
|
frame["weaponKills"] = tally.WeaponKills;
|
||||||
|
if (tally.Headshots > 0)
|
||||||
|
frame["headshots"] = tally.Headshots;
|
||||||
|
if (tally.BestPvpM > 0f)
|
||||||
|
frame["bestPvpM"] = (float)Math.Round(tally.BestPvpM, 1);
|
||||||
|
if (tally.BestPveM > 0f)
|
||||||
|
frame["bestPveM"] = (float)Math.Round(tally.BestPveM, 1);
|
||||||
|
if (tally.ApcKills > 0)
|
||||||
|
frame["apcKills"] = tally.ApcKills;
|
||||||
|
if (tally.HeliKills > 0)
|
||||||
|
frame["heliKills"] = tally.HeliKills;
|
||||||
|
if (tally.Crafted.Count > 0)
|
||||||
|
frame["crafted"] = tally.Crafted;
|
||||||
|
if (tally.Built > 0)
|
||||||
|
frame["built"] = tally.Built;
|
||||||
|
if (tally.Repaired > 0)
|
||||||
|
frame["repaired"] = tally.Repaired;
|
||||||
|
if (tally.Healed > 0)
|
||||||
|
frame["healed"] = tally.Healed;
|
||||||
|
if (tally.Rockets > 0)
|
||||||
|
frame["rockets"] = tally.Rockets;
|
||||||
|
if (tally.Explosives > 0)
|
||||||
|
frame["explosives"] = tally.Explosives;
|
||||||
|
if (tally.Missions > 0)
|
||||||
|
frame["missions"] = tally.Missions;
|
||||||
|
|
||||||
EmitFrame(frame);
|
EmitFrame(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user