C#:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Obituary", "yellow51", "0.1.0")]
[Description("A characterful kill feed with multikills, killstreaks, nemeses, revenge, first blood and death recap cards.")]
public class Obituary : RustPlugin
{
#region Fields
private const string RecapUiName = "obituary.recap";
private Configuration _config;
private StoredData _data;
private readonly Dictionary<ulong, int> _streaks = new Dictionary<ulong, int>();
private readonly Dictionary<ulong, ComboState> _combos = new Dictionary<ulong, ComboState>();
private readonly Dictionary<ulong, KillContext> _lastDeaths = new Dictionary<ulong, KillContext>();
private readonly Dictionary<ulong, Dictionary<ulong, int>> _killMatrix = new Dictionary<ulong, Dictionary<ulong, int>>();
private class ComboState
{
public int Count;
public float LastKillTime;
}
private class KillContext
{
public BaseCombatEntity VictimEntity;
public BasePlayer VictimPlayer;
public VictimKind VictimKind;
public Vector3 VictimPos;
public BasePlayer Killer;
public string KillerName;
public string VictimName;
public string Weapon;
public string Ammo;
public float DistanceMeters;
public bool Headshot;
public float KillerHpRemaining;
public Cause Cause;
public bool KillerIsPlayer => Killer != null;
public bool VictimIsPlayer => VictimKind == VictimKind.Player;
public bool IsPvp => KillerIsPlayer && VictimIsPlayer;
}
private enum VictimKind { None, Player, Npc, Animal }
private enum Cause
{
Player, Scientist, HeavyScientist, Murderer, Scarecrow, Zombie, Gingerbread,
TunnelDweller, UnderwaterDweller, Shopkeeper, CustomNpc,
Animal, Shark, BeeSwarm,
Bradley, Helicopter, Minicopter, ScrapHelicopter, AttackHelicopter,
Train, MagnetCrane, Elevator, HotAirBalloon, BatteringRam, Tugboat, CargoShip,
ExcavatorArm, CarShredder, Drone,
Turret, Sentry, Sam, Trap, Fire, HeatSource,
Wall, Barricade, Lock, Cactus,
Fall, Drowned, Cold, Heat, Radiation, Bleeding, Hunger, Thirst, Poison,
ElectricShock, Collision, Explosion, Suicide,
Unknown
}
private static readonly Dictionary<string, Cause> KillerTypeNames = new Dictionary<string, Cause>(StringComparer.OrdinalIgnoreCase)
{
["BasePlayer"] = Cause.Player,
["Scientist"] = Cause.Scientist,
["ScientistNPC"] = Cause.Scientist,
["ScientistNPCNew"] = Cause.Scientist,
["ScientistNPC2"] = Cause.Scientist,
["HTNPlayer"] = Cause.Scientist,
["NPCMurderer"] = Cause.Murderer,
["ScarecrowNPC"] = Cause.Scarecrow,
["scarecrow"] = Cause.Scarecrow,
["Zombie"] = Cause.Zombie,
["GingerbreadNPC"] = Cause.Gingerbread,
["tunneldweller"] = Cause.TunnelDweller,
["underwaterdweller"] = Cause.UnderwaterDweller,
["NPCShopKeeper"] = Cause.Shopkeeper,
["BradleyAPC"] = Cause.Bradley,
["PatrolHelicopter"] = Cause.Helicopter,
["BaseHelicopter"] = Cause.Helicopter,
["Minicopter"] = Cause.Minicopter,
["ScrapTransportHelicopter"] = Cause.ScrapHelicopter,
["AttackHelicopter"] = Cause.AttackHelicopter,
["TrainEngine"] = Cause.Train,
["TrainCar"] = Cause.Train,
["MagnetCrane"] = Cause.MagnetCrane,
["ElevatorLift"] = Cause.Elevator,
["HotAirBalloon"] = Cause.HotAirBalloon,
["BatteringRam"] = Cause.BatteringRam,
["Tugboat"] = Cause.Tugboat,
["CargoShip"] = Cause.CargoShip,
["ExcavatorArm"] = Cause.ExcavatorArm,
["LargeShredder"] = Cause.CarShredder,
["Drone"] = Cause.Drone,
["BeeSwarmAI"] = Cause.BeeSwarm,
["SimpleShark"] = Cause.Shark,
["AutoTurret"] = Cause.Turret,
["FlameTurret"] = Cause.Turret,
["NPCAutoTurret"] = Cause.Sentry,
["SamSite"] = Cause.Sam,
["GunTrap"] = Cause.Trap,
["BaseTrap"] = Cause.Trap,
["FireBall"] = Cause.Fire,
["BaseOven"] = Cause.HeatSource,
["SimpleBuildingBlock"] = Cause.Wall,
["Barricade"] = Cause.Barricade,
["CodeLock"] = Cause.Lock,
["DigitSendCodeLock"] = Cause.Lock,
["Wolf2"] = Cause.Animal,
["Panther"] = Cause.Animal,
["Tiger"] = Cause.Animal,
["Crocodile"] = Cause.Animal,
["SnakeHazard"] = Cause.Animal,
["RidableHorse"] = Cause.Animal,
["FarmableAnimal"] = Cause.Animal,
["BaseAnimalNPC"] = Cause.Animal,
["BaseFishNPC"] = Cause.Animal
};
#endregion
#region Configuration
private class Configuration
{
[JsonProperty("Default distance unit (meters | yards | feet)")]
public string DistanceUnit = "meters";
[JsonProperty("Allow players to set their own distance unit with /obit unit")]
public bool AllowPlayerDistanceUnit = true;
[JsonProperty("Long shot threshold in meters (uses the long-shot lines above this)")]
public float LongShotMeters = 200f;
[JsonProperty("Broadcast radius in meters for the kill feed (0 = whole server)")]
public float BroadcastRadius = 0f;
[JsonProperty("Show the basic kill feed line")]
public bool ShowKillFeed = true;
[JsonProperty("Announce player deaths")]
public bool AnnouncePlayers = true;
[JsonProperty("Announce NPC deaths (scientists, murderers, zombies, dwellers, etc.)")]
public bool AnnounceNpcs = true;
[JsonProperty("Announce animal deaths")]
public bool AnnounceAnimals = true;
[JsonProperty("Announce deaths that involve no player at all (NPC vs NPC, etc.)")]
public bool AnnounceNonPlayerInvolved = true;
[JsonProperty("Only announce animal deaths a player caused, or that another animal caused near a player (suppresses wilderness/unknown animal deaths)")]
public bool AnimalDeathsNeedPlayerOrWitness = true;
[JsonProperty("Announce one animal killing another (only when a player is within the witness radius below)")]
public bool AnnounceAnimalVsAnimal = true;
[JsonProperty("Player vicinity radius, in meters, for witnessing an animal-on-animal kill")]
public float AnimalWitnessRadius = 30f;
[JsonProperty("Count NPC and animal kills toward killstreaks and multikills")]
public bool CountNpcKillsTowardStreaks = true;
[JsonProperty("First blood requires a player victim")]
public bool FirstBloodPlayersOnly = true;
[JsonProperty("Longest shot record requires a player victim")]
public bool LongestShotPlayersOnly = true;
[JsonProperty("Show multikill announcements")]
public bool ShowMultikills = true;
[JsonProperty("Show killstreak announcements")]
public bool ShowStreaks = true;
[JsonProperty("Show first blood announcement")]
public bool ShowFirstBlood = true;
[JsonProperty("Show nemesis / revenge announcements")]
public bool ShowNemesis = true;
[JsonProperty("Show longest-shot-of-the-wipe announcement")]
public bool ShowLongestShot = true;
[JsonProperty("Show the death recap card on respawn")]
public bool ShowRecapCard = true;
[JsonProperty("Recap card duration in seconds")]
public float RecapDuration = 8f;
[JsonProperty("Reset all player stats on wipe")]
public bool ResetStatsOnWipe = true;
[JsonProperty("Seconds between kills to keep a multikill combo alive")]
public float ComboWindow = 5f;
[JsonProperty("Kills required before someone becomes a nemesis")]
public int NemesisThreshold = 3;
[JsonProperty("Times a player must have killed you before killing them counts as revenge")]
public int RevengeThreshold = 3;
[JsonProperty("Colors")]
public ColorConfig Colors = new ColorConfig();
[JsonProperty("Multikill tiers (combo count -> name)")]
public List<Tier> MultikillTiers = new List<Tier>
{
new Tier { Threshold = 2, Name = "Double Kill" },
new Tier { Threshold = 3, Name = "Triple Kill" },
new Tier { Threshold = 4, Name = "Multi Kill" },
new Tier { Threshold = 5, Name = "MEGA KILL" },
new Tier { Threshold = 6, Name = "MONSTER KILL" }
};
[JsonProperty("Killstreak tiers (kills -> name)")]
public List<Tier> StreakTiers = new List<Tier>
{
new Tier { Threshold = 3, Name = "on a Killing Spree" },
new Tier { Threshold = 5, Name = "Rampaging" },
new Tier { Threshold = 7, Name = "Unstoppable" },
new Tier { Threshold = 10, Name = "Dominating" },
new Tier { Threshold = 12, Name = "Godlike" },
new Tier { Threshold = 15, Name = "Beyond Godlike" }
};
[JsonProperty("Message pools (category -> lines)")]
public Dictionary<string, List<string>> Messages = DefaultMessages();
public static Dictionary<string, List<string>> DefaultMessages() => new Dictionary<string, List<string>>
{
["player"] = new List<string>
{
"{killer} ventilated {victim} with a {weapon} ({ammo}) from {distance}. The body cooled before it hit the dirt.",
"{killer} redecorated the rocks with {victim}'s insides. {weapon} ({ammo}), {distance}.",
"{victim} took a {weapon} ({ammo}) to the chest from {distance} and folded like wet cardboard.",
"{killer} turned {victim} into a leaking sack of meat. {weapon} ({ammo}), {distance}.",
"{victim} got opened up by {killer}'s {weapon} ({ammo}) at {distance}. Loot's still warm.",
"{killer} sent {victim} back to the main menu. {weapon} ({ammo}), {distance}.",
"{victim} caught a bad case of sudden lead poisoning courtesy of {killer} ({distance}).",
"GG {victim}, no re. {killer} did the honors with a {weapon} ({ammo}).",
"{killer} ratio'd {victim} in real life from {distance}. {weapon} ({ammo}).",
"{victim} got served, and the special was a {weapon} ({ammo}). {distance}.",
"FINISH HIM. {killer} did, from {distance}.",
"{victim} got John Wick'd by {killer}. {weapon} ({ammo}), {distance}.",
"{killer} ended {victim}'s whole career from {distance} with a {weapon} ({ammo}).",
"Wasted. {killer} dropped {victim} at {distance}.",
"{victim} pressed F to pay respects to themselves after {killer}'s {weapon} ({ammo}).",
"{killer} uninstalled {victim} from existence. {weapon} ({ammo}), {distance}.",
"{victim} found out, after a lengthy period of fckin around, that {killer} had a {weapon} ({ammo}).",
"{killer} hit {victim} with the no-scope from {distance}. {weapon} ({ammo}).",
"{victim} chose violence. {killer} chose better. {weapon} ({ammo}), {distance}.",
"{killer} turned {victim} off like a light switch. {weapon} ({ammo}), {distance}.",
"{victim} got deleted faster than {killer}'s browser history. {weapon} ({ammo}).",
"{victim} took the L and a faceful of {ammo} from {killer} at {distance}.",
"{killer} mailed {victim} a {weapon} ({ammo}) from {distance}. Return to sender: denied.",
"{victim} got one-shotted like a tutorial mob by {killer}. {distance}.",
"{killer} put {victim} in the dirt and the dirt said thanks. {weapon} ({ammo}), {distance}.",
"{victim} yeeted off the mortal coil by {killer}'s {weapon} ({ammo}) at {distance}.",
"{killer} speedran {victim}'s funeral. {weapon} ({ammo}), {distance}.",
"{victim} got cooked by {killer}, and not the fun way. {weapon} ({ammo}), {distance}."
},
["headshot"] = new List<string>
{
"{killer} uncorked {victim}'s skull with a {weapon} ({ammo}) at {distance}. Brain mist included.",
"Headshot. {victim}'s last thought is decorating the landscape, courtesy of {killer} ({distance}).",
"{killer} popped {victim}'s head like an overripe melon. {weapon} ({ammo}), {distance}.",
"One round, one less skull. {killer} emptied {victim}'s head from {distance}.",
"BOOM, headshot. {victim} never saw {killer}'s {weapon} ({ammo}) coming. {distance}.",
"{killer} gave {victim} a third eye and took the other two. {weapon} ({ammo}), {distance}.",
"{victim} got their cranium ratio'd by {killer} from {distance}. {weapon} ({ammo}).",
"Critical hit. {killer} turned {victim}'s head into modern art. {weapon} ({ammo}), {distance}.",
"{killer} clicked on {victim}'s head from {distance}. Aimbot allegations incoming. {weapon} ({ammo}).",
"{victim} lost the staring contest and their forehead. {killer}, {weapon} ({ammo}), {distance}.",
"Snip snap, {victim}'s skull is gone. {killer} from {distance}. {weapon} ({ammo}).",
"{killer} headshot {victim} so clean the loot didn't even flinch. {distance}.",
"{victim} got their brains rebooted to factory settings by {killer}. {weapon} ({ammo}), {distance}.",
"Top of the morning, top of {victim}'s head, gone. {killer}, {distance}.",
"{killer} gave {victim} a new part in their hairline. Permanent. {weapon} ({ammo}), {distance}.",
"{victim} caught {killer}'s {weapon} ({ammo}) with their face at {distance}. Bad catch."
},
["longshot"] = new List<string>
{
"{killer} reached across {distance} and erased {victim} before the sound caught up. {weapon} ({ammo}).",
"{victim} never even heard the {weapon} ({ammo}). {killer} collected a corpse from {distance}.",
"From {distance}, {killer} turned {victim} into a distant red smear. {weapon} ({ammo}).",
"{distance}? {killer} clipped {victim} like it was point blank. {weapon} ({ammo}).",
"{victim} got sniped from {distance}. {killer} doesn't believe in close range.",
"{killer} mailed {victim} a {weapon} ({ammo}) from {distance}. Express delivery.",
"Across {distance}, {killer} reminded {victim} that the map is smaller than it looks. {weapon} ({ammo}).",
"{victim} was a tiny dot, then no dot. {killer} from {distance}. {weapon} ({ammo}).",
"{killer} hit {victim} from {distance}. Hand of God, but the angry one. {weapon} ({ammo}).",
"Long range, short life. {killer} dropped {victim} at {distance}. {weapon} ({ammo}).",
"{victim} got cross-mapped by {killer}. {distance} of pure disrespect. {weapon} ({ammo}).",
"{killer} touched {victim} from {distance} without ever getting close. {weapon} ({ammo})."
},
["animal"] = new List<string>
{
"{victim} got disemboweled by a wild {killer}. Nature doesn't do mercy.",
"A {killer} dragged {victim} down and ate the screaming parts first.",
"A {killer} turned {victim} inside out and left the rest for the maggots.",
"{victim} became a {killer}'s lunch. Bones cracked, then silence.",
"{victim} lost the Hunger Games to a {killer}.",
"A {killer} RKO'd {victim} outta nowhere.",
"{victim} got got. The food chain remains undefeated. ({killer})",
"Nature is metal, and {victim} is now a track on the album. ({killer})",
"A {killer} speedran {victim}'s digestive tract.",
"{victim} tried to pet a {killer}. The {killer} disagreed.",
"A {killer} turned {victim} into an Attenborough documentary. The sad part.",
"{victim} brought a rock to a {killer} fight. It went how you'd think.",
"A wild {killer} appeared. {victim} did not run. {victim} should have run.",
"{victim} got mauled into next wipe by a {killer}."
},
["shark"] = new List<string>
{
"{victim} got chewed into chum by {killer}. The water went red and stayed that way.",
"{killer} took {victim} in one bite. Somewhere a fin is still grinning.",
"{victim} needed a bigger boat. They got a {killer} instead.",
"{victim} learned the ocean keeps everything it kills.",
"Baby shark did {victim}, dun dun dun. ({killer})",
"{victim} found out Sharknado was basically a documentary. ({killer})",
"{killer} turned {victim} into a seafood platter for one."
},
["npc"] = new List<string>
{
"A scientist double-tapped {victim} and stepped over the cooling body.",
"{victim} got clinically executed by an NPC. No paperwork, no mercy.",
"A scientist put {victim} down and went right back to patrolling.",
"NPC diff. {victim} got outplayed by a bundle of if-statements.",
"{victim} got peer-reviewed by a scientist. Rejected, with prejudice.",
"A scientist tactically deleted {victim} and didn't even reload.",
"{victim} failed the lab safety briefing. Fatally.",
"A lab coat with a gun ended {victim}. Embarrassing tombstone."
},
["zombie"] = new List<string>
{
"{victim} got swarmed and devoured. The undead don't chew, they just take.",
"A zombie tore {victim} open and wore the leftovers.",
"{victim} got dragged into the horde and came apart at the seams.",
"Braaains. {victim} had one. Briefly.",
"{victim} failed the zombie apocalypse tutorial.",
"{victim} got Left 4 Dead. Mostly the dead part.",
"{victim} respawned in the food group."
},
["scarecrow"] = new List<string>
{
"A scarecrow gutted {victim} and left them for the crows.",
"{victim} got hollowed out by a scarecrow. Stuffing not included.",
"{victim} lost a knife fight to a sack of straw. Bold tombstone.",
"A scarecrow spooked {victim} clean off the mortal coil."
},
["bradley"] = new List<string>
{
"Bradley painted the road with {victim}. Scrape what's left into a sleeping bag.",
"{victim} caught a tank shell and became a fine red fog.",
"Bradley left a smoking crater where {victim} used to stand.",
"{victim} tried to 1v1 a tank. Bold. Stupid. Dead.",
"Bradley said no. Bradley always says no.",
"{victim} got main-cannoned into a thin paste."
},
["heli"] = new List<string>
{
"The patrol helicopter shredded {victim} into wet confetti. Closed casket, if there were a casket.",
"{victim} got vaporized by the heli. Even the loot looked scared.",
"Rockets in, {victim} out. The heli doesn't take prisoners.",
"Get to the choppa. {victim} did not.",
"{victim} heard Ride of the Valkyries, then heard nothing.",
"The heli rained fire and {victim} became weather."
},
["turret"] = new List<string>
{
"A turret stitched {victim} from groin to throat before they could blink.",
"{victim} walked into a turret and got hosed down to the bone.",
"A turret emptied a full mag into {victim} and politely asked for more.",
"{victim} got turned into a colander. So many holes, so little dignity.",
"The turret went brrrrt. {victim} went limp.",
"Aimbot? No. Just a turret. {victim} is just built different (dead).",
"{victim} got dakka'd into the afterlife.",
"A turret introduced {victim} to its entire belt of ammo at once."
},
["trap"] = new List<string>
{
"{victim} found {killer} the hard way - jaws first, screaming second.",
"{killer} snapped shut on {victim} and didn't let go until the kicking stopped.",
"{victim} got mangled by {killer}. The trap reset itself, satisfied.",
"It's a trap. {victim} found {killer}. Admiral Ackbar warned them.",
"{victim} got Home Alone'd by {killer}. Less comedy, more carnage.",
"{victim} stepped in {killer}. Click. Crunch. Done."
},
["explosion"] = new List<string>
{
"{victim} was reduced to a fine red mist and a pair of empty boots.",
"The blast unzipped {victim} and scattered the pieces across three foundations.",
"{victim} found the boom. The boom found every bone {victim} owned.",
"{victim} got atomized. Friends are still picking bits of {victim} out of their hair.",
"All that was left of {victim} was a crater and the faint stink of copper.",
"Boom goes the {victim}.",
"{victim} went out with a bang. Several, actually.",
"{victim} reenacted Mythbusters, the part where it goes very wrong.",
"{victim} found out C4 means see-you-in-four-pieces.",
"{victim} did a speedrun of being everywhere at once.",
"{victim} got disassembled molecule by molecule.",
"{victim} achieved maximum entropy. Loot rained for a solid second.",
"{victim} got Looney Tunes'd. Where's the anvil.",
"GG, {victim} is now a fine paste on the wall."
},
["fire"] = new List<string>
{
"{victim} cooked alive, screaming, skin sloughing off in sheets.",
"Fire ate {victim} from the outside in. The smell lingered for days.",
"{victim} burned down to a blackened, grinning husk.",
"{victim} ran flailing and shrieking until the flames won. They always win.",
"{victim} got rendered down to charcoal and regret.",
"This is fine, said {victim}, on fire, lying.",
"{victim} got Ghost Rider'd, minus the cool.",
"Gordon Ramsay rates {victim}: finally, some good f***ing charcoal.",
"{victim} became the campfire everyone gathered around.",
"Hot Pocket {victim}: surface of the sun on the inside.",
"{victim} learned fire beats flesh, every single time."
},
["cold"] = new List<string>
{
"{victim} froze solid mid-shiver, eyes wide open. They'll thaw by spring.",
"The cold crept into {victim}'s chest and switched off the organs one at a time.",
"{victim} became a frostbitten statue. Brittle. Snappable.",
"{victim} stopped shivering, then stopped breathing, then stopped being a person.",
"{victim} froze like Windows 95.",
"{victim} blue-screened in the snow.",
"{victim} got put on ice. Mr. Freeze nodded approvingly.",
"Winter came. {victim} did not survive it.",
"{victim} turned into a grape popsicle, judging by the color."
},
["electric"] = new List<string>
{
"{victim} cooked from the inside - eyes boiling, fillings melting.",
"Electricity tore through {victim} and left them twitching, smoking, and very dead.",
"{victim} grabbed the wrong wire and became a meat lightning rod.",
"{victim} convulsed once, hard, dropped like a dead battery, and stank of burnt hair.",
"{victim} got Palpatine'd. Unlimited power, zero survivors.",
"{victim} did the Electric Slide, permanently.",
"Zeus says hi. {victim} says nothing, ever again.",
"{victim} stuck a fork in the metaphorical outlet.",
"{victim}: 100 percent charged, then instantly bricked.",
"{victim} got Raiden'd. Toasty."
},
["heat"] = new List<string>
{
"{victim} basted in their own juices until the lights went out.",
"Heat wrung {victim} dry and left a cracked husk.",
"{victim} got slow-roasted. Low and slow, the wasteland way.",
"Heat 1, {victim} 0."
},
["fall"] = new List<string>
{
"{victim} hit the ground so hard their legs went up through their ribs.",
"Gravity collected {victim}. The landing was... chunky.",
"{victim} forgot they couldn't fly and discovered they could splatter.",
"{victim} folded into a shape humans aren't supposed to make.",
"{victim} took the scenic route down. Terminal velocity included.",
"{victim} did a trust fall. Nobody was there.",
"{victim} discovered fast travel. Straight down.",
"{victim} pulled a full Wile E. Coyote. Beep beep, splat.",
"Geronimooo, said {victim}, briefly.",
"{victim} found out the ground is undefeated.",
"{victim} fus-ro-dah'd themselves clean off a cliff.",
"{victim} respawned at terminal velocity into the dirt."
},
["drowned"] = new List<string>
{
"{victim} sucked in the sea and sank with the fish. Bloated and blue by morning.",
"The water filled {victim} up and decided to keep them.",
"{victim} thrashed, gulped, and went still beneath the surface.",
"{victim} discovered they aren't Aquaman.",
"{victim} got SpongeBob'd, minus the breathing-underwater part.",
"Glub glub. {victim} is fish food now.",
"{victim} forgot to git gud at swimming.",
"{victim} sleeps with the fishes."
},
["radiation"] = new List<string>
{
"{victim} cooked from the inside out - glowing, puking, then nothing.",
"Radiation melted {victim} into a sludgy, glowing puddle.",
"{victim} sloughed apart in the green haze and called it a day.",
"{victim} forgot their meds. Fallout taught them nothing.",
"{victim} glows in the dark now. Spooky and dead.",
"{victim} leveled up into a ghoul, then into a corpse.",
"{victim} got the three-eyed-fish special."
},
["bleeding"] = new List<string>
{
"{victim} bled out one warm pulse at a time, alone, staring at the sky.",
"{victim} left a long red trail and a very short life.",
"{victim} ran out of blood before they ran out of bad decisions.",
"{victim} forgot bandages exist. Natural selection at work.",
"{victim} left more red on the ground than a Tarantino film.",
"{victim} ran out of red juice and ideas at the same time."
},
["hunger"] = new List<string>
{
"{victim} starved down to a skeleton with the skin still on.",
"{victim}'s stomach ate itself, then quietly gave up.",
"{victim} got hangry, then just dead.",
"{victim} forgot food was a survival mechanic.",
"{victim}'s stomach filed for divorce and won."
},
["thirst"] = new List<string>
{
"{victim} dried out from the inside, tongue like leather, lips split to the gum.",
"{victim} died chasing water they never found.",
"{victim} needed a Capri Sun and got a tombstone.",
"{victim} forgot water exists. The wasteland did not."
},
["poison"] = new List<string>
{
"{victim} foamed, seized, and folded over their own guts.",
"Poison turned {victim} inside out, slowly, while they watched.",
"{victim} ate the mystery meat. Bold choice. Last choice.",
"{victim} drank the wrong potion and met the floor.",
"{victim} got Breaking Bad'd by their own dinner."
},
["suicide"] = new List<string>
{
"{victim} fumbled their own grenade and disassembled themselves.",
"{victim} lost a 1v1 with their own C4. Spectacularly.",
"{victim} blew themselves to bits and saved everyone the ammo.",
"{victim} did the server's killing for it. Messily.",
"{victim} pressed the big red button. Oops.",
"{victim} did a full Leeroy Jenkins.",
"{victim} alt-F4'd their own existence.",
"{victim} fragged themselves. Friendly fire isn't so friendly.",
"{victim} won the Darwin Award, server edition."
},
["generic"] = new List<string>
{
"{killer} ground {victim} into the dirt and didn't slow down.",
"{victim} got mulched by {killer}. Hose it off and move on.",
"{killer} ended {victim} in a way the medics would rather not discuss.",
"{victim} met {killer} and came apart at the seams.",
"{killer} introduced {victim} to physics. Physics won.",
"{victim} lost an argument with {killer}. Decisively and wetly.",
"{killer} turned {victim} into a cautionary tale.",
"{victim} got Final Destination'd by {killer}.",
"{killer} ran the numbers on {victim}. The answer was zero."
},
["unknown"] = new List<string>
{
"{victim} died screaming at nothing. The wasteland keeps its secrets.",
"{victim} just stopped. Body's still warm, cause is a mystery.",
"{victim} ceased. No notes, no killcam.",
"404: {victim}'s life not found.",
"{victim} disconnected from existence. No reason given.",
"Something got {victim}. We may never know what."
}
};
}
private class ColorConfig
{
[JsonProperty("Killer")] public string Killer = "#C4FF00";
[JsonProperty("Victim")] public string Victim = "#FF6B6B";
[JsonProperty("Weapon")] public string Weapon = "#FAB005";
[JsonProperty("Distance")] public string Distance = "#74C0FC";
[JsonProperty("Accent")] public string Accent = "#FFD93D";
}
private class Tier
{
[JsonProperty("Threshold")] public int Threshold;
[JsonProperty("Name")] public string Name;
}
protected override void LoadDefaultConfig()
{
_config = new Configuration();
PrintWarning("Created a new configuration file.");
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<Configuration>();
if (_config == null) throw new Exception("Configuration is null.");
if (_config.Messages == null || _config.Messages.Count == 0)
_config.Messages = Configuration.DefaultMessages();
}
catch (Exception ex)
{
PrintError($"Configuration file is invalid; using defaults. Error: {ex.Message}");
LoadDefaultConfig();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(_config);
#endregion
#region Stored data
private class StoredData
{
public string WipeToken = "";
public bool FirstBloodClaimed = false;
public LongestShot Longest = new LongestShot();
public Dictionary<ulong, PlayerStats> Players = new Dictionary<ulong, PlayerStats>();
public Dictionary<ulong, string> Units = new Dictionary<ulong, string>();
}
private class PlayerStats
{
public string Name = "";
public int Kills;
public int Deaths;
public int BestStreak;
public float LongestShotMeters;
}
private class LongestShot
{
public float Meters;
public string Killer = "";
public string Victim = "";
public string Weapon = "";
}
private void LoadData()
{
try { _data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name); }
catch { _data = null; }
if (_data == null) _data = new StoredData();
if (_data.Players == null) _data.Players = new Dictionary<ulong, PlayerStats>();
if (_data.Units == null) _data.Units = new Dictionary<ulong, string>();
if (_data.Longest == null) _data.Longest = new LongestShot();
}
private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(Name, _data);
private PlayerStats Stats(BasePlayer player)
{
if (!_data.Players.TryGetValue(player.userID, out var s))
{
s = new PlayerStats { Name = player.displayName };
_data.Players[player.userID] = s;
}
s.Name = player.displayName;
return s;
}
#endregion
#region Lifecycle hooks
private void Init()
{
LoadData();
}
private void OnServerInitialized()
{
CheckWipe();
}
private void OnNewSave(string filename)
{
ResetWipeState();
}
private void Unload()
{
foreach (BasePlayer player in BasePlayer.activePlayerList)
CuiHelper.DestroyUi(player, RecapUiName);
SaveData();
}
private void CheckWipe()
{
string token = $"{World.Seed}_{World.Size}";
if (_data.WipeToken != token)
{
_data.WipeToken = token;
ResetWipeState(token);
}
}
private void ResetWipeState(string token = null)
{
_streaks.Clear();
_combos.Clear();
_killMatrix.Clear();
_lastDeaths.Clear();
_data.FirstBloodClaimed = false;
_data.Longest = new LongestShot();
if (token != null) _data.WipeToken = token;
if (_config.ResetStatsOnWipe) _data.Players.Clear();
SaveData();
}
#endregion
#region Death handling
private void OnEntityDeath(BaseCombatEntity entity, HitInfo info)
{
if (entity == null) return;
VictimKind kind = ClassifyVictim(entity);
if (kind == VictimKind.None)
return;
KillContext ctx = BuildContext(entity, info, kind);
int victimEndedStreak = 0;
if (ctx.VictimIsPlayer)
{
Stats(ctx.VictimPlayer).Deaths++;
victimEndedStreak = _streaks.TryGetValue(ctx.VictimPlayer.userID, out var vs) ? vs : 0;
_streaks[ctx.VictimPlayer.userID] = 0;
_lastDeaths[ctx.VictimPlayer.userID] = ctx;
}
if (ctx.KillerIsPlayer)
HandlePlayerKill(ctx, victimEndedStreak);
if (_config.ShowKillFeed && ShouldAnnounce(ctx, kind))
{
string line = ComposeFeedLine(ctx);
if (!string.IsNullOrEmpty(line))
BroadcastKill(line, ctx);
}
SaveData();
}
private bool ShouldAnnounce(KillContext ctx, VictimKind kind)
{
switch (kind)
{
case VictimKind.Player:
return _config.AnnouncePlayers;
case VictimKind.Npc:
if (!_config.AnnounceNpcs) return false;
if (ctx.KillerIsPlayer || ctx.VictimIsPlayer) return true;
return _config.AnnounceNonPlayerInvolved;
case VictimKind.Animal:
return ShouldAnnounceAnimal(ctx);
default:
return false;
}
}
private bool ShouldAnnounceAnimal(KillContext ctx)
{
if (!_config.AnnounceAnimals) return false;
if (ctx.KillerIsPlayer) return true;
if (!_config.AnimalDeathsNeedPlayerOrWitness) return true;
if (_config.AnnounceAnimalVsAnimal && ctx.Cause == Cause.Animal)
return PlayerWithin(ctx.VictimPos, _config.AnimalWitnessRadius);
return false;
}
private bool PlayerWithin(Vector3 position, float radius)
{
if (radius <= 0f) return false;
float sqr = radius * radius;
foreach (BasePlayer p in BasePlayer.activePlayerList)
{
if (p == null || !p.IsConnected || p.IsNpc || !p.userID.IsSteamId()) continue;
if ((p.transform.position - position).sqrMagnitude <= sqr)
return true;
}
return false;
}
private void HandlePlayerKill(KillContext ctx, int victimEndedStreak)
{
BasePlayer killer = ctx.Killer;
PlayerStats ks = Stats(killer);
ks.Kills++;
bool counts = ctx.VictimIsPlayer || _config.CountNpcKillsTowardStreaks;
if (_config.ShowFirstBlood && !_data.FirstBloodClaimed)
{
bool eligible = ctx.IsPvp || !_config.FirstBloodPlayersOnly;
if (eligible)
{
_data.FirstBloodClaimed = true;
Announce($"<color={_config.Colors.Accent}>FIRST BLOOD!</color> {PlayerName(killer)} drew it on {VictimColored(ctx)}.");
}
}
if (counts)
{
int streak = (_streaks.TryGetValue(killer.userID, out var s) ? s : 0) + 1;
_streaks[killer.userID] = streak;
if (streak > ks.BestStreak) ks.BestStreak = streak;
if (_config.ShowStreaks)
{
if (ctx.IsPvp && victimEndedStreak >= (_config.StreakTiers.FirstOrDefault()?.Threshold ?? 3))
Announce($"{PlayerName(killer)} ended {PlayerName(ctx.VictimPlayer)}'s {victimEndedStreak}-kill streak. Somewhere, a crowd cheers.");
string tier = ExactTierName(_config.StreakTiers, streak);
if (tier != null)
Announce($"{PlayerName(killer)} is <color={_config.Colors.Accent}>{tier}</color>! ({streak} kills)");
}
}
if (_config.ShowMultikills && counts)
{
ComboState combo = UpdateCombo(killer.userID);
if (combo.Count >= 2)
{
string tier = TierName(_config.MultikillTiers, combo.Count);
if (tier != null)
Announce($"{PlayerName(killer)}: <color={_config.Colors.Accent}>{tier}</color>!");
}
}
if (ctx.IsPvp)
{
if (_config.ShowNemesis)
{
int killerWasKilledBy = GetKills(ctx.VictimPlayer.userID, killer.userID);
if (killerWasKilledBy >= _config.RevengeThreshold)
{
Announce($"<color={_config.Colors.Accent}>REVENGE!</color> {PlayerName(killer)} finally got {PlayerName(ctx.VictimPlayer)} back.");
SetKills(ctx.VictimPlayer.userID, killer.userID, 0);
}
int newCount = AddKill(killer.userID, ctx.VictimPlayer.userID);
if (newCount == _config.NemesisThreshold)
Announce($"{PlayerName(killer)} has become {PlayerName(ctx.VictimPlayer)}'s <color={_config.Colors.Victim}>nemesis</color> ({newCount} kills and counting).");
}
else
{
AddKill(killer.userID, ctx.VictimPlayer.userID);
}
}
if (ctx.DistanceMeters > ks.LongestShotMeters)
ks.LongestShotMeters = ctx.DistanceMeters;
bool shotEligible = ctx.VictimIsPlayer || !_config.LongestShotPlayersOnly;
if (_config.ShowLongestShot && shotEligible &&
ctx.DistanceMeters > _data.Longest.Meters && ctx.DistanceMeters >= _config.LongShotMeters)
{
_data.Longest = new LongestShot
{
Meters = ctx.DistanceMeters,
Killer = killer.displayName,
Victim = ctx.VictimName,
Weapon = ctx.Weapon
};
Announce($"<color={_config.Colors.Accent}>NEW LONGEST SHOT OF THE WIPE!</color> {PlayerName(killer)} dropped {VictimColored(ctx)} at {FormatDistance(ctx.DistanceMeters)}.");
}
}
private void OnPlayerRespawned(BasePlayer player)
{
if (player == null || !_config.ShowRecapCard) return;
if (!_lastDeaths.TryGetValue(player.userID, out KillContext ctx)) return;
ShowRecapCard(player, ctx);
}
#endregion
#region Context building
private VictimKind ClassifyVictim(BaseCombatEntity entity)
{
BasePlayer p = entity as BasePlayer;
if (p != null)
return (!p.IsNpc && p.userID.IsSteamId()) ? VictimKind.Player : VictimKind.Npc;
switch (ClassifyKiller(entity))
{
case Cause.Animal:
case Cause.Shark:
case Cause.BeeSwarm:
return VictimKind.Animal;
case Cause.Scientist:
case Cause.HeavyScientist:
case Cause.Murderer:
case Cause.Scarecrow:
case Cause.Zombie:
case Cause.Gingerbread:
case Cause.TunnelDweller:
case Cause.UnderwaterDweller:
case Cause.Shopkeeper:
case Cause.CustomNpc:
return VictimKind.Npc;
default:
return VictimKind.None;
}
}
private KillContext BuildContext(BaseCombatEntity victim, HitInfo info, VictimKind kind)
{
var ctx = new KillContext
{
VictimEntity = victim,
VictimKind = kind,
VictimPlayer = kind == VictimKind.Player ? victim as BasePlayer : null,
VictimPos = victim.transform.position,
Cause = Cause.Unknown
};
ctx.VictimName = kind == VictimKind.Player
? ctx.VictimPlayer.displayName
: GetCharacterName(victim, kind);
ctx.Weapon = GetWeaponName(info);
ctx.Ammo = GetAmmoName(info);
BaseEntity initiator = info?.Initiator;
BasePlayer attacker = info?.InitiatorPlayer ?? ResolveOwner(initiator);
if (attacker != null && !attacker.IsNpc && attacker.userID.IsSteamId() && attacker != victim)
{
ctx.Killer = attacker;
ctx.KillerName = attacker.displayName;
ctx.Cause = Cause.Player;
ctx.Headshot = info?.isHeadshot ?? false;
ctx.KillerHpRemaining = attacker.health;
ctx.DistanceMeters = Vector3.Distance(attacker.transform.position, ctx.VictimPos);
return ctx;
}
Cause cause = ClassifyKiller(initiator);
if (cause == Cause.Unknown)
cause = MapDamageType(info, ctx.VictimPlayer, attacker);
ctx.Cause = cause;
ctx.KillerName = DefaultKillerLabel(cause, initiator);
return ctx;
}
private Cause ClassifyKiller(BaseEntity entity)
{
if (entity == null)
return Cause.Unknown;
string typeName = entity.GetType().Name;
string prefab = (entity.ShortPrefabName ?? "").ToLowerInvariant();
if (prefab.Contains("shark")) return Cause.Shark;
if (prefab.Contains("cactus")) return Cause.Cactus;
if (prefab.Contains("tunneldweller")) return Cause.TunnelDweller;
if (prefab.Contains("underwaterdweller")) ret
// ... [Code preview truncated due to length. Full source in attached file] ...