Иконка ресурса

FREE Obituary : 0.1.0 0.1.0

Нет прав для скачивания
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] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху