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

FREE RHGKillstreakRewards : 1.2.0 1.2.0

Нет прав для скачивания
C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("RHG Killstreak Rewards", "RHG", "1.2.0")]
    [Description("Awards tiered rewards and temporary micro-buffs at 3/5/8/10/12/15/20 kill milestones.")]
    public class RHGKillstreakRewards : CovalencePlugin
    {
        #region Permissions & Consts

        private const string PermAdmin = "rhgkillstreakrewards.admin";
        private const string UiName = "RHGKS.UI";
        private const string UiBuffName = "RHGKS.BUFF";
        private const string UiStreakBreakName = "RHGKS.SBREAK";
        private const float UiDefaultSeconds = 4f;

        #endregion

        #region Data Models

        private class RewardItem
        {
            public string ShortName;
            public int Amount;
            public ulong Skin;
        }

        private class BuffSpec
        {
            public float OutgoingDamageMultiplier = 1.00f;
            public float IncomingDamageMultiplier = 1.00f;
            public float SpeedMultiplier = 1.00f;
            public float HealPerKill = 0f;
            public float DurationSeconds = 30f;
            public string Label = "";
        }

        private class ThresholdReward
        {
            public int Kills;
            public List<RewardItem> Items = new List<RewardItem>();
            public BuffSpec Buff = new BuffSpec();
            public string AnnounceText = "";
            public string TierColor = "1 0.5 0 0.85";
        }

        private class AntiAbuse
        {
            public bool PreventSameVictimChain = true;
            public float MinSecondsSameVictim = 20f;
            public bool CountOnlyPlayers = true;
            public bool RequireAttackerAlive = true;
        }

        private class UiConfig
        {
            public bool Enabled = true;
            public float ShowSeconds = UiDefaultSeconds;
            public string AnchorMin = "0.35 0.88";
            public string AnchorMax = "0.65 0.99";
            public float BackgroundAlpha = 0.75f;
            public bool BuffBarEnabled = true;
            public string BuffBarAnchorMin = "0.78 0.12";
            public string BuffBarAnchorMax = "0.995 0.185";
        }

        private class AnnounceConfig
        {
            public bool BroadcastChat = true;
            public string ChatPrefix = "<color=#7FFFD4>[KS]</color>";
            public bool NotifyVictimOfStreak = true;
            public bool BroadcastStreakBreak = true;
            public int StreakBreakMinKills = 5;
        }

        private class KSConfig
        {
            public List<ThresholdReward> Rewards = new List<ThresholdReward>();
            public AntiAbuse AntiAbuse = new AntiAbuse();
            public UiConfig UI = new UiConfig();
            public AnnounceConfig Announce = new AnnounceConfig();
        }

        private class PlayerState
        {
            public int Streak = 0;
            public int TopStreak = 0;
            public ulong LastVictimId = 0;
            public double LastVictimTime = 0;
            public Timer BuffTimer = null;
            public BuffSpec ActiveBuff = null;
            public double BuffEndTime = 0;
            public Timer BuffBarTimer = null;
        }

        #endregion

        #region Fields

        private KSConfig _config;
        private readonly Dictionary<ulong, PlayerState> _states = new Dictionary<ulong, PlayerState>();
        private readonly Dictionary<ulong, double> _lastInputApply = new Dictionary<ulong, double>();

        // ── PI integration (optional) ─────────────────────────────────────
        [PluginReference] private Plugin RHGPlayerIntelligence;

        // Event reward multiplier set by RHGEventIntelligence via KSR_SetEventMultiplier.
        // Scoped per event ID so multiple concurrent events don't conflict.
        private readonly Dictionary<string, float> _eventMultipliers = new Dictionary<string, float>();

        // PI local risk cache — maintained via PI_OnPlayerUpdated push hook.
        // Used to adjust reward item amounts: trusted players get slightly more, high-risk get less.
        private readonly Dictionary<ulong, int>    _piRiskCache  = new Dictionary<ulong, int>();
        private readonly Dictionary<ulong, int>    _piTrustCache = new Dictionary<ulong, int>();
        private readonly Dictionary<ulong, string> _piTagCache   = new Dictionary<ulong, string>();

        private void PI_OnPlayerUpdated(string userId, int risk, int trust, string tag, int rep)
        {
            ulong uid;
            if (string.IsNullOrEmpty(userId) || !ulong.TryParse(userId, out uid)) return;
            _piRiskCache[uid]  = risk;
            _piTrustCache[uid] = trust;
            _piTagCache[uid]   = tag;
        }

        #endregion

        #region External API

        // Called by RHGEventIntelligence to set a DNA-learned reward multiplier for an active event.
        // eventId: the event ID string. multiplier: e.g. 1.3 = 30% more items.
        // Pass 0f or 1f to clear/reset.
        [HookMethod("KSR_SetEventMultiplier")]
        public void KSR_SetEventMultiplier(string eventId, float multiplier)
        {
            if (string.IsNullOrEmpty(eventId)) return;
            if (multiplier <= 0f || Math.Abs(multiplier - 1f) < 0.001f)
                _eventMultipliers.Remove(eventId);
            else
                _eventMultipliers[eventId] = multiplier;
        }

        // Called by RHGEventIntelligence when an event ends to clean up the multiplier.
        [HookMethod("KSR_ClearEventMultiplier")]
        public void KSR_ClearEventMultiplier(string eventId)
        {
            if (!string.IsNullOrEmpty(eventId))
                _eventMultipliers.Remove(eventId);
        }

        // Returns the current streak count for a player (0 if not tracked).
        [HookMethod("KSR_GetStreak")]
        public int KSR_GetStreak(string userId)
        {
            ulong uid;
            if (!ulong.TryParse(userId, out uid)) return 0;
            PlayerState s;
            return _states.TryGetValue(uid, out s) ? s.Streak : 0;
        }

        #endregion

        #region Oxide Hooks

        private void Init()
        {
            permission.RegisterPermission(PermAdmin, this);
            LoadConfigValues();
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<KSConfig>();
                if (_config == null) throw new Exception("Config null");
            }
            catch
            {
                PrintWarning("Config is corrupted, resetting to defaults.");
                LoadDefaultConfig();
            }
        }

        protected override void LoadDefaultConfig()
        {
            _config = DefaultConfig();
            SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(_config, true);

        private KSConfig DefaultConfig()
        {
            return new KSConfig
            {
                Rewards = new List<ThresholdReward>
                {
                    new ThresholdReward
                    {
                        Kills = 3,
                        Items = new List<RewardItem>
                        {
                            new RewardItem{ ShortName="scrap", Amount=50 },
                            new RewardItem{ ShortName="metal.fragments", Amount=100 },
                            new RewardItem{ ShortName="ammo.rifle", Amount=50 }
                        },
                        Buff = new BuffSpec
                        {
                            OutgoingDamageMultiplier = 1.05f,
                            IncomingDamageMultiplier = 1.00f,
                            DurationSeconds = 30f,
                            Label = "+5% Damage (30s)"
                        },
                        AnnounceText = "🔥 {player} is on a 3 Kill Streak!",
                        TierColor = "1 0.55 0.05 0.85"
                    },
                    new ThresholdReward
                    {
                        Kills = 5,
                        Items = new List<RewardItem>
                        {
                            new RewardItem{ ShortName="scrap", Amount=150 },
                            new RewardItem{ ShortName="hq.metal.ore", Amount=200 },
                            new RewardItem{ ShortName="pistol.python", Amount=1 }
                        },
                        Buff = new BuffSpec
                        {
                            OutgoingDamageMultiplier = 1.10f,
                            IncomingDamageMultiplier = 1.00f,
                            DurationSeconds = 45f,
                            Label = "+10% Damage (45s)"
                        },
                        AnnounceText = "⚡ {player} is rising with a 5 Kill Streak!",
                        TierColor = "1 0.15 0.1 0.85"
                    },
                    new ThresholdReward
                    {
                        Kills = 8,
                        Items = new List<RewardItem>
                        {
                            new RewardItem{ ShortName="scrap", Amount=300 },
                            new RewardItem{ ShortName="sulfur.ore", Amount=500 },
                            new RewardItem{ ShortName="explosive.timed", Amount=1 }
                        },
                        Buff = new BuffSpec
                        {
                            OutgoingDamageMultiplier = 1.00f,
                            IncomingDamageMultiplier = 0.85f,
                            DurationSeconds = 60f,
                            Label = "-15% Incoming Damage (60s)"
                        },
                        AnnounceText = "🏆 {player} is dominating with an 8 Kill Streak!",
                        TierColor = "0.72 0.3 1 0.9"
                    },
                    new ThresholdReward
                    {
                        Kills = 10,
                        Items = new List<RewardItem>
                        {
                            new RewardItem{ ShortName="sulfur.ore", Amount=500 },
                            new RewardItem{ ShortName="metal.fragments", Amount=300 },
                            new RewardItem{ ShortName="syringe.medical", Amount=2 }
                        },
                        Buff = new BuffSpec
                        {
                            SpeedMultiplier = 1.15f,
                            DurationSeconds = 25f,
                            Label = "+15% Speed (25s)"
                        },
                        AnnounceText = "💨 {player} hits 10 Kill Streak — fast as the wind!",
                        TierColor = "0.05 0.75 0.85 0.88"
                    },
                    new ThresholdReward
                    {
                        Kills = 12,
                        Items = new List<RewardItem>
                        {
                            new RewardItem{ ShortName="scrap", Amount=400 },
                            new RewardItem{ ShortName="hq.metal.ore", Amount=500 },
                            new RewardItem{ ShortName="rifle.ak", Amount=1 }
                        },
                        Buff = new BuffSpec
                        {
                            OutgoingDamageMultiplier = 1.12f,
                            IncomingDamageMultiplier = 0.90f,
                            DurationSeconds = 50f,
                            Label = "+12% Damage / -10% Incoming (50s)"
                        },
                        AnnounceText = "💎 {player} 12 Kill Streak — unstoppable!",
                        TierColor = "0.85 0.15 0.75 0.88"
                    },
                    new ThresholdReward
                    {
                        Kills = 15,
                        Items = new List<RewardItem>
                        {
                            new RewardItem{ ShortName="scrap", Amount=600 },
                            new RewardItem{ ShortName="largemedkit", Amount=3 },
                            new RewardItem{ ShortName="ammo.rocket.hv", Amount=5 }
                        },
                        Buff = new BuffSpec
                        {
                            HealPerKill = 15f,
                            DurationSeconds = 40f,
                            Label = "Vampire: +15 HP per Kill (40s)"
                        },
                        AnnounceText = "🧛 {player} 15 Kill Streak — vampire mode activated!",
                        TierColor = "1 0.75 0.0 0.92"
                    },
                    new ThresholdReward
                    {
                        Kills = 20,
                        Items = new List<RewardItem>
                        {
                            new RewardItem{ ShortName="scrap", Amount=1000 },
                            new RewardItem{ ShortName="sulfur.ore", Amount=1000 },
                            new RewardItem{ ShortName="explosive.timed", Amount=5 },
                            new RewardItem{ ShortName="rocket.launcher", Amount=1 }
                        },
                        Buff = new BuffSpec
                        {
                            OutgoingDamageMultiplier = 1.20f,
                            IncomingDamageMultiplier = 0.80f,
                            SpeedMultiplier = 1.10f,
                            HealPerKill = 10f,
                            DurationSeconds = 90f,
                            Label = "LEGEND: +20% DMG / -20% INC / +10% SPD / 10 HP/Kill (90s)"
                        },
                        AnnounceText = "👑 {player} 20 KILL STREAK — LEGEND MODE ACTIVATED!",
                        TierColor = "1 0.92 0.3 0.97"
                    }
                },
                AntiAbuse = new AntiAbuse(),
                UI = new UiConfig(),
                Announce = new AnnounceConfig
                {
                    BroadcastChat = true,
                    ChatPrefix = "<color=#7FFFD4>[KS]</color>",
                    NotifyVictimOfStreak = true,
                    BroadcastStreakBreak = true,
                    StreakBreakMinKills = 5
                }
            };
        }

        private void OnServerInitialized()
        {
            foreach (var p in BasePlayer.activePlayerList)
                EnsureState(p.userID);
        }

        private void OnPlayerConnected(BasePlayer player)
        {
            EnsureState(player.userID);
        }

        private void Unload()
        {
            foreach (var p in BasePlayer.activePlayerList)
            {
                if (p == null) continue;
                ClearBuff(p.userID);
                DestroyUi(p);
                DestroyBuffBar(p);
                DestroyStreakBreakUi(p);
            }
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            ClearState(player.userID);
        }

        private void OnPlayerRespawned(BasePlayer player)
        {
            ResetStreak(player);
        }

        private void OnEntityDeath(BaseCombatEntity entity, HitInfo info)
        {
            try
            {
                if (entity == null || info == null) return;
                var victim = entity as BasePlayer;
                if (victim == null || victim.userID == 0) return;
                // CountOnlyPlayers gate: ignore NPC/bot kills
                if (_config.AntiAbuse.CountOnlyPlayers && !victim.userID.IsSteamId()) return;

                var attacker = info.InitiatorPlayer;
                if (attacker == null || attacker.userID == 0) return;
                if (attacker.userID == victim.userID) return;
                if (_config.AntiAbuse.RequireAttackerAlive && !attacker.IsAlive()) return;
                // CanCountKill BEFORE AnnounceStreakBreak so abused kills don't trigger announcements
                if (!CanCountKill(attacker.userID, victim.userID)) return;
                AnnounceStreakBreak(victim, attacker);

                IncrementStreak(attacker, victim);
            }
            catch (Exception e)
            {
                PrintWarning($"OnEntityDeath error: {e}");
            }
        }

        private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
        {
            try
            {
                if (entity == null || info == null) return null;

                var attacker = info.InitiatorPlayer;
                if (attacker != null && _states.TryGetValue(attacker.userID, out var s) && s.ActiveBuff != null)
                {
                    if (s.ActiveBuff.OutgoingDamageMultiplier != 1.0f)
                        info.damageTypes.ScaleAll(s.ActiveBuff.OutgoingDamageMultiplier);
                }

                var targetPlayer = entity as BasePlayer;
                if (targetPlayer != null && _states.TryGetValue(targetPlayer.userID, out var s2) && s2.ActiveBuff != null)
                {
                    if (s2.ActiveBuff.IncomingDamageMultiplier != 1.0f)
                        info.damageTypes.ScaleAll(s2.ActiveBuff.IncomingDamageMultiplier);
                }
            }
            catch (Exception e)
            {
                PrintWarning($"OnEntityTakeDamage error: {e}");
            }
            return null;
        }

        private void OnPlayerInput(BasePlayer player, InputState input)
        {
            try
            {
                if (player == null || input == null) return;
                if (!_states.TryGetValue(player.userID, out var state)) return;
                if (state.ActiveBuff == null || state.ActiveBuff.SpeedMultiplier <= 1.0f) return;
                if (!player.IsAlive() || player.IsSleeping()) return;

                // Throttle to max once per 50 ms to reduce per-frame CPU load
                var now = Interface.Oxide.Now;
                double lastApply;
                if (_lastInputApply.TryGetValue(player.userID, out lastApply) && now - lastApply < 0.05) return;
                _lastInputApply[player.userID] = now;

                float extra = (state.ActiveBuff.SpeedMultiplier - 1.0f) * 0.09f;
                var dir = Vector3.zero;
                if (input.IsDown(BUTTON.FORWARD)) dir += player.eyes.BodyForward();
                if (input.IsDown(BUTTON.BACKWARD)) dir -= player.eyes.BodyForward();
                if (input.IsDown(BUTTON.RIGHT)) dir += player.eyes.BodyRight();
                if (input.IsDown(BUTTON.LEFT)) dir -= player.eyes.BodyRight();
                dir.y = 0f;

                if (dir.sqrMagnitude < 0.01f) return;
                dir.Normalize();
                player.MovePosition(player.transform.position + dir * extra);
            }
            catch { }
        }

        private object OnPlayerViolation(BasePlayer player, AntiHackType type, float amount)
        {
            if (player == null) return null;
            if (!_states.TryGetValue(player.userID, out var state)) return null;
            if (state.ActiveBuff == null || state.ActiveBuff.SpeedMultiplier <= 1.0f) return null;
            if (type == AntiHackType.FlyHack || type == AntiHackType.SpeedHack)
                return true;
            return null;
        }

        #endregion

        #region Core Logic

        private void IncrementStreak(BasePlayer attacker, BasePlayer victim)
        {
            var now = Interface.Oxide.Now;
            var state = EnsureState(attacker.userID);
            state.Streak++;
            if (state.Streak > state.TopStreak) state.TopStreak = state.Streak;
            state.LastVictimId = victim.userID;
            state.LastVictimTime = now;

            if (_config.Announce.NotifyVictimOfStreak && state.Streak >= 3 && victim.IsConnected)
                victim.ChatMessage($"{_config.Announce.ChatPrefix} <color=#FF6B6B>☠ {attacker.displayName} killed you while on a x{state.Streak} Kill Streak!</color>");

            // HealPerKill — restore health on each kill if buff is active
            if (state.ActiveBuff != null && state.ActiveBuff.HealPerKill > 0f)
            {
                float newHealth = Mathf.Min(attacker.health + state.ActiveBuff.HealPerKill, attacker.MaxHealth());
                attacker.SetHealth(newHealth);
                attacker.ChatMessage($"{_config.Announce.ChatPrefix} <color=#7CFC00>+{state.ActiveBuff.HealPerKill} HP (Vampire)</color>");
            }

            var reward = _config.Rewards.OrderBy(r => r.Kills).LastOrDefault(r => r.Kills == state.Streak);
            if (reward != null)
            {
                GiveReward(attacker, reward);
                ApplyBuff(attacker, reward.Buff);
                Announce(attacker.displayName, reward);
                ShowUi(attacker, state.Streak, reward);
                // Milestone sound effect
                Effect.server.Run("assets/prefabs/misc/xmas/presents/effects/unwrap.prefab", attacker.transform.position);
            }
            else ShowUi(attacker, state.Streak, null);
        }

        private bool CanCountKill(ulong attackerId, ulong victimId)
        {
            var s = EnsureState(attackerId);
            if (_config.AntiAbuse.PreventSameVictimChain &&
                s.LastVictimId == victimId &&
                Interface.Oxide.Now - s.LastVictimTime < _config.AntiAbuse.MinSecondsSameVictim)
                return false;
            return true;
        }

        private void GiveReward(BasePlayer player, ThresholdReward reward)
        {
            // ── Step 1: Event DNA multiplier (set by RHGEventIntelligence) ──────────────
            float mult = 1.0f;
            if (_eventMultipliers.Count > 0)
            {
                float best = 1.0f;
                foreach (var kv in _eventMultipliers)
                    if (kv.Value > best) best = kv.Value;
                mult = best;
            }

            // ── Step 2: PI behavior gate + scaling ───────────────────────────────────────
            // When RHGPlayerIntelligence is loaded, BehaviorTag and RiskScore drive rewards.
            // This is what turns the free plugin into a premium experience with core installed.
            //
            //  Toxic / Griefer / Menace → reward BLOCKED (0 items)
            //  Risk > 70               → 50% reward (soft deterrent)
            //  Trusted / Veteran       → +20% bonus
            //  Survivor / Hunter       → +10% bonus
            //  Others                  → neutral (no change)
            if (RHGPlayerIntelligence != null)
            {
                string tag;
                int risk;
                _piTagCache.TryGetValue(player.userID, out tag);
                _piRiskCache.TryGetValue(player.userID, out risk);

                // Hard block: toxic players get no kill rewards
                if (tag == "Toxic" || tag == "Griefer" || tag == "Menace")
                {
                    player.ChatMessage(string.Concat(
                        "<color=#ff4444>[KS]</color> Your behavior record (",
                        tag ?? "unknown",
                        ") blocks streak rewards."));
                    return; // skip items entirely — buff still applied by caller
                }

                // Soft penalty: high risk (not yet tagged as toxic — warning zone)
                if (risk > 70)
                    mult *= 0.5f;

                // Bonus: experienced/trusted players
                if (tag == "Veteran" || tag == "Trusted")
                    mult *= 1.20f;
                else if (tag == "Survivor" || tag == "Hunter")
                    mult *= 1.10f;
            }

            // ── Step 3: Give items with final multiplier ──────────────────────────────────
            foreach (var it in reward.Items)
            {
                var def = ItemManager.FindItemDefinition(it.ShortName);
                if (def == null) continue;
                int baseAmt = Math.Max(1, Mathf.RoundToInt(it.Amount * mult));
                int left = baseAmt;
                while (left > 0)
                {
                    int give = Math.Min(left, def.stackable > 0 ? def.stackable : left);
                    var item = ItemManager.Create(def, give, it.Skin);
                    if (!item.MoveToContainer(player.inventory.containerMain) &&
                        !item.MoveToContainer(player.inventory.containerBelt))
                        item.Drop(player.transform.position + Vector3.up, Vector3.zero);
                    left -= give;
                }
            }
        }

        private void ApplyBuff(BasePlayer player, BuffSpec buff)
        {
            ClearBuff(player.userID);
            if (buff == null) return;
            var state = EnsureState(player.userID);
            state.ActiveBuff = buff;
            state.BuffEndTime = Interface.Oxide.Now + buff.DurationSeconds;
            state.BuffTimer = timer.Once(buff.DurationSeconds, () =>
            {
                var p = BasePlayer.FindByID(player.userID);
                ClearBuff(player.userID);
                if (p != null && p.IsConnected)
                {
                    ShowUi(p, state.Streak, null, "<color=#FF8C00>⚡ Buff expired!</color>");
                    p.ChatMessage($"{_config.Announce.ChatPrefix} <color=#FFA500>Your buff has ended: {buff.Label}</color>");
                }
            });
            // Draw initial bar then use Repeat (one persistent timer instead of recursive Once)
            DrawBuffBar(player, buff, state);
            if (_config.UI.BuffBarEnabled)
            {
                state.BuffBarTimer = timer.Repeat(1f, 0, () =>
                {
                    if (!player.IsConnected || !_states.TryGetValue(player.userID, out var st) || st.ActiveBuff == null)
                    {
                        DestroyBuffBar(player);
                        state.BuffBarTimer?.Destroy();
                        state.BuffBarTimer = null;
                        return;
                    }
                    DrawBuffBar(player, buff, st);
                });
            }
        }

        private void ClearBuff(ulong userID)
        {
            if (_states.TryGetValue(userID, out var s))
            {
                s.ActiveBuff = null;
                s.BuffEndTime = 0;
                s.BuffTimer?.Destroy();
                s.BuffTimer = null;
                s.BuffBarTimer?.Destroy();
                s.BuffBarTimer = null;
            }
            var player = BasePlayer.FindByID(userID);
            if (player != null) DestroyBuffBar(player);
        }

        private void ResetStreak(BasePlayer player)
        {
            var s = EnsureState(player.userID);
            s.Streak = 0;
            s.LastVictimId = 0;
            s.LastVictimTime = 0;
            ClearBuff(player.userID);
            DestroyUi(player);
            DestroyStreakBreakUi(player);
        }

        private PlayerState EnsureState(ulong userID)
        {
            if (!_states.TryGetValue(userID, out var s))
            {
                s = new PlayerState();
                _states[userID] = s;
            }
            return s;
        }

        private void ClearState(ulong userID)
        {
            if (_states.TryGetValue(userID, out var s))
            {
                s.BuffTimer?.Destroy();
                s.BuffBarTimer?.Destroy();
                _states.Remove(userID);
            }
        }

        #endregion

        #region UI & Announce

        private void Announce(string playerName, ThresholdReward reward)
        {
            if (!_config.Announce.BroadcastChat || reward == null) return;
            var msg = reward.AnnounceText.Replace("{player}", playerName);
            server.Broadcast($"{_config.Announce.ChatPrefix} {msg}");
            Interface.CallHook("KSR_OnStreakReached", playerName, reward.Kills, msg);
        }

        private void ShowUi(BasePlayer player, int streak, ThresholdReward reward, string custom = null)
        {
            if (!_config.UI.Enabled || player == null) return;
            DestroyUi(player);

            var nextReward = _config.Rewards.OrderBy(r => r.Kills).FirstOrDefault(r => r.Kills > streak);
            int killsToNext = nextReward != null ? nextReward.Kills - streak : 0;
            string bgColor = reward != null ? reward.TierColor : $"0.05 0.05 0.05 {_config.UI.BackgroundAlpha}";

            var container = new CuiElementContainer();
            var bg = container.Add(new CuiPanel
            {
                Image = { Color = bgColor },
                RectTransform = { AnchorMin = _config.UI.AnchorMin, AnchorMax = _config.UI.AnchorMax },
                CursorEnabled = false
            }, "Overlay", UiName);

            // Accent bar at top for reward tiers
            if (reward != null)
                container.Add(new CuiPanel
                {
                    Image = { Color = "1 1 1 0.20" },
                    RectTransform = { AnchorMin = "0 0.91", AnchorMax = "1 1" }
                }, bg);

            // Title row
            string titleText = custom ?? $"🔥  KILLSTREAK  x{streak}";
            container.Add(new CuiLabel
            {
                Text = { Text = titleText, FontSize = 17, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1", Font = "robotocondensed-bold.ttf" },
                RectTransform = { AnchorMin = "0.03 0.54", AnchorMax = "0.97 0.90" }
            }, bg);

            // Middle row: reward items or active buff
            string midText = "";
            if (custom == null)
            {
                if (reward != null)
                    midText = string.Join("  ·  ", reward.Items.Select(i => $"{i.Amount}x {i.ShortName}"));
                else
                {
                    var st = EnsureState(player.userID);
                    if (st.ActiveBuff != null) midText = $"Active Buff: {st.ActiveBuff.Label}";
                }
            }
            if (!string.IsNullOrEmpty(midText))
                container.Add(new CuiLabel
                {
                    Text = { Text = midText, FontSize = 11, Align = TextAnchor.MiddleCenter, Color = "1 1 0.65 1" },
                    RectTransform = { AnchorMin = "0.03 0.28", AnchorMax = "0.97 0.54" }
                }, bg);

            // Bottom row: buff label or next threshold hint
            string bottomText = "";
            if (custom == null)
            {
                if (reward != null)
                    bottomText = $"⚡ {reward.Buff?.Label ?? ""}";
                else if (nextReward != null)
                    bottomText = $"Next reward in {killsToNext} kill(s)  (threshold: {nextReward.Kills})";
                else
                    bottomText = "All thresholds conquered! 🏆";
            }
            if (!string.IsNullOrEmpty(bottomText))
                container.Add(new CuiLabel
                {
                    Text = { Text = bottomText, FontSize = 11, Align = TextAnchor.MiddleCenter, Color = "0.8 0.95 1 0.9" },
                    RectTransform = { AnchorMin = "0.03 0.04", AnchorMax = "0.97 0.28" }
                }, bg);

            CuiHelper.AddUi(player, container);
            timer.Once(Mathf.Max(1f, _config.UI.ShowSeconds), () => DestroyUi(player));
        }

        private void DestroyUi(BasePlayer player)
        {
            if (player == null) return;
            CuiHelper.DestroyUi(player, UiName);
        }

        // ShowBuffBar kept for backward compat (external calls); delegates to DrawBuffBar
        private void ShowBuffBar(BasePlayer player, BuffSpec buff, PlayerState state)
        {
            DrawBuffBar(player, buff, state);
        }

        private void DrawBuffBar(BasePlayer player, BuffSpec buff, PlayerState state)
        {
            if (!_config.UI.BuffBarEnabled || player == null || buff == null) return;
            DestroyBuffBar(player);

            double remaining = state.BuffEndTime - Interface.Oxide.Now;
            if (remaining <= 0) return;

            float fraction = Mathf.Clamp01((float)(remaining / buff.DurationSeconds));
            var container = new CuiElementContainer();
            var bg = container.Add(new CuiPanel
            {
                Image = { Color = "0.04 0.04 0.04 0.82" },
                RectTransform = { AnchorMin = _config.UI.BuffBarAnchorMin, AnchorMax = _config.UI.BuffBarAnchorMax },
                CursorEnabled = false
            }, "Hud", UiBuffName);

            // Bar track
            container.Add(new CuiPanel
            {
                Image = { Color = "0.18 0.18 0.18 1" },
                RectTransform = { AnchorMin = "0.02 0.06", AnchorMax = "0.98 0.38" }
            }, bg);

            // Active fill
            if (fraction > 0f)
                container.Add(new CuiPanel
                {
                    Image = { Color = "0.2 0.85 0.45 0.9" },
                    RectTransform = { AnchorMin = "0.02 0.06", AnchorMax = $"{0.02f + 0.96f * fraction:F3} 0.38" }
                }, bg);

            // Label
            var parts = new List<string>();
            if (buff.OutgoingDamageMultiplier != 1.0f) parts.Add($"+{(int)((buff.OutgoingDamageMultiplier - 1f) * 100)}% DMG");
            if (buff.IncomingDamageMultiplier != 1.0f) parts.Add($"-{(int)((1f - buff.IncomingDamageMultiplier) * 100)}% INC");
            if (buff.SpeedMultiplier != 1.0f) parts.Add($"+{(int)((buff.SpeedMultiplier - 1f) * 100)}% SPD");
            if (buff.HealPerKill > 0f) parts.Add($"{buff.HealPerKill} HP/Kill");
            string summary = parts.Count > 0 ? string.Join("  ·  ", parts) : buff.Label;
            container.Add(new CuiLabel
            {
                Text = { Text = $"⚡ {summary}   {(int)remaining}s", FontSize = 12, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" },
                RectTransform = { AnchorMin = "0.02 0.40", AnchorMax = "0.98 0.96" }
            }, bg);

            CuiHelper.AddUi(player, container);
        }

        private void DestroyBuffBar(BasePlayer player)
        {
            if (player == null) return;
            CuiHelper.DestroyUi(player, UiBuffName);
        }

        private void DestroyStreakBreakUi(BasePlayer player)
        {
            if (player == null) return;
            CuiHelper.DestroyUi(player, UiStreakBreakName);
        }

        private void ShowStreakBreakUi(BasePlayer player, string victimName, int streak)
        {
            if (!_config.UI.Enabled || player == null) return;
            DestroyStreakBreakUi(player);

            var container = new CuiElementContainer();
            var bg = container.Add(new CuiPanel
            {
                Image = { Color = "0.05 0.55 0.15 0.85" },
                RectTransform = { AnchorMin = _config.UI.AnchorMin, AnchorMax = _config.UI.AnchorMax },
                CursorEnabled = false
            }, "Overlay", UiStreakBreakName);

            container.Add(new CuiLabel
            {
                Text = { Text = "🎯  STREAK BROKEN!", FontSize = 18, Align = TextAnchor.MiddleCenter, Color = "1 1 0.2 1", Font = "robotocondensed-bold.ttf" },
                RectTransform = { AnchorMin = "0.03 0.52", AnchorMax = "0.97 0.92" }
            }, bg);

            container.Add(new CuiLabel
            {
                Text = { Text = $"You ended {victimName}'s x{streak} Kill Streak!", FontSize = 12, Align = TextAnchor.MiddleCenter, Color = "0.9 1 0.9 1" },
                RectTransform = { AnchorMin = "0.03 0.08", AnchorMax = "0.97 0.52" }
            }, bg);

            CuiHelper.AddUi(player, container);
            timer.Once(Mathf.Max(1f, _config.UI.ShowSeconds), () => DestroyStreakBreakUi(player));
        }

        private void AnnounceStreakBreak(BasePlayer victim, BasePlayer attacker)
        {
            if (!_states.TryGetValue(victim.userID, out var victimState)) return;
            int streak = victimState.Streak;
            if (streak < _config.Announce.StreakBreakMinKills) return;

            attacker.ChatMessage($"{_config.Announce.ChatPrefix} <color=#FFD700>🎯 You ended {victim.displayName}'s x{streak} Kill Streak!</color>");

            if (_config.Announce.BroadcastStreakBreak)
                server.Broadcast($"{_config.Announce.ChatPrefix} <color=#FF6347>💥 {attacker.displayName} ended {victim.displayName}'s x{streak} Kill Streak!</color>");

            Interface.CallHook("KSR_OnStreakBroken", attacker.UserIDString, attacker.displayName, victim.displayName, streak);

            ShowStreakBreakUi(attacker, victim.displayName, streak);
        }

        #endregion

        #region Commands

        [Command("ks")]
        private void CmdKS(IPlayer iplayer, string command, string[] args)
        {
            var bp = iplayer.Object as BasePlayer;
            if (bp == null) { iplayer.Reply("In-game use only."); return; }
            var s = EnsureState(bp.userID);
            var buff = s.ActiveBuff != null ? s.ActiveBuff.Label : "None";
            iplayer.Reply($"[KS] Streak: {s.Streak} | Buff: {buff}");
        }

        [Command("ks.reset")]
        private void CmdKSReset(IPlayer iplayer, string command, string[] args)
        {
            if (!iplayer.HasPermission(PermAdmin)) { iplayer.Reply("No permission."); return; }
            if (args.Length < 1) { iplayer.Reply("Usage: /ks.reset <name|id>"); return; }
            var target = FindPlayer(args[0]);
            if (target?.Object is BasePlayer bp)
            {
                ResetStreak(bp);
                iplayer.Reply($"[KS] {bp.displayName}'s streak has been reset.");
            }
            else iplayer.Reply("Player not found.");
        }

        [Command("ks.reload")]
        private void CmdKSReload(IPlayer iplayer, string command, string[] args)
        {
            if (!iplayer.HasPermission(PermAdmin)) { iplayer.Reply("No permission."); return; }
            LoadConfigValues();
            iplayer.Reply("[KS] Config reloaded.");
        }

        [Command("ks.test")]
        private void CmdKSTest(IPlayer iplayer, string command, string[] args)
        {
            if (!iplayer.HasPermission(PermAdmin)) { iplayer.Reply("No permission."); return; }
            if (args.Length < 2) { iplayer.Reply("Usage: /ks.test <name|id> <3|5|8|10|12|15|20>"); return; }
            var target = FindPlayer(args[0]);
            if (!(target?.Object is BasePlayer bp)) { iplayer.Reply("Player not found."); return; }
            if (!int.TryParse(args[1], out var thr) || !_config.Rewards.Any(r => r.Kills == thr))
            {
                iplayer.Reply("Invalid threshold. Valid values: 3, 5, 8, 10, 12, 15, 20");
                return;
            }
            var rew = _config.Rewards.First(r => r.Kills == thr);
            // Sync streak state so UI shows the correct value
            var st = EnsureState(bp.userID);
            st.Streak = thr;
            if (st.Streak > st.TopStreak) st.TopStreak = st.Streak;
            GiveReward(bp, rew);
            ApplyBuff(bp, rew.Buff);
            Announce(bp.displayName, rew);
            ShowUi(bp, thr, rew);
            iplayer.Reply($"[KS] Test reward ({thr} kills) applied to {bp.displayName}.");
        }

        [Command("ks.setstreak")]
        private void CmdKSSetStreak(IPlayer iplayer, string command, string[] args)
        {
            if (!iplayer.HasPermission(PermAdmin)) { iplayer.Reply("No permission."); return; }
            if (args.Length < 2) { iplayer.Reply("Usage: /ks.setstreak <name|id> <amount>"); return; }
            var target = FindPlayer(args[0]);
            if (!(target?.Object is BasePlayer bp)) { iplayer.Reply("Player not found."); return; }
            if (!int.TryParse(args[1], out var amount) || amount < 0) { iplayer.Reply("Invalid amount (must be >= 0)."); return; }
            var st = EnsureState(bp.userID);
            st.Streak = amount;
            if (amount > st.TopStreak) st.TopStreak = amount;
            iplayer.Reply($"[KS] {bp.displayName}'s streak set to {amount}.");
            bp.ChatMessage($"{_config.Announce.ChatPrefix} <color=#FFA500>Your streak was set to {amount} by an admin.</color>");
        }

        [Command("ks.top")]
        private void CmdKSTop(IPlayer iplayer, string command, string[] args)
        {
            var bp = iplayer.Object as BasePlayer;
            var entries = new List<KeyValuePair<string, int>>();
            foreach (var kv in _states)
            {
                var p = BasePlayer.FindByID(kv.Key);
                if (p == null) continue;
                entries.Add(new KeyValuePair<string, int>(p.displayName, kv.Value.TopStreak));
            }
            entries.Sort((a, b) => b.Value.CompareTo(a.Value));
            var sb = new System.Text.StringBuilder();
            sb.AppendLine($"{_config.Announce.ChatPrefix} <color=#FFD700>★ Top Killstreaks (Session)</color>");
            int rank = 1;
            foreach (var e in entries)
            {
                if (rank > 10) break;
                sb.AppendLine($"  <color=#aaaaaa>{rank}.</color> {e.Key}  <color=#FFD700>{e.Value}</color> kills");
                rank++;
            }
            if (entries.Count == 0) sb.AppendLine("  No data yet.");
            iplayer.Reply(sb.ToString());
        }

        #endregion

        #region Helpers

        private void LoadConfigValues()
        {
            LoadConfig();
            SaveConfig();
        }

        private IPlayer FindPlayer(string nameOrId)
        {
            var target = covalence.Players.FindPlayerById(nameOrId) ?? covalence.Players.FindPlayer(nameOrId);
            return target?.IsConnected == true ? target : null;
        }

        #endregion
    }
}

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