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

FREE OmniRPGEngine : 0.3.1 0.3.1

Нет прав для скачивания
C#:
// OmniRPGEngine.cs
// Core XP + Rage discipline system with UI shell, leaderboard, admin config editor, and BotReSpawn profile XP UI.
// NOTE: Drop into oxide/plugins (or carbon/plugins) as-is. Data/config files will be created on first load.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Oxide.Core;
using Oxide.Core.Configuration;
using Oxide.Core.Plugins;
using Newtonsoft.Json;
using Oxide.Game.Rust.Cui;
using Rust;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("OmniRPGEngine", "Somescrub", "0.3.1")]
    [Description("Universal RPG framework providing XP, levels and discipline-based skill trees (Rage MVP).")]
    public class OmniRPGEngine : RustPlugin
    {
        #region Permissions / References

        private const string PERM_USE = "omnirpgengine.use";
        private const string PERM_ADMIN = "omnirpgengine.admin";

        [PluginReference] private Plugin PermissionsManager;
        [PluginReference("ImageLibrary")] private Plugin ImageLibrary;
        [PluginReference("BotReSpawn")] private Plugin BotReSpawn; // Used to detect BotReSpawn NPCs
        [PluginReference]
        private Plugin Economics;

        [PluginReference]
        private Plugin ServerRewards;

        // Track BotReSpawn NPC userIDs so they never pollute player stats / leaderboard
        private readonly HashSet<ulong> botReSpawnIds = new HashSet<ulong>();

        // Per-player Bot XP page index for pagination
        private readonly Dictionary<ulong, int> botXpPage = new Dictionary<ulong, int>();

        #endregion

        #region Config

        private PluginConfig config;

        private class PluginConfig
        {
            public string DataFileName = "OmniRPGEngine_Data";

            public XpSettings XP = new XpSettings();
            public RageSettings Rage = new RageSettings();
            public UiSettings UI = new UiSettings();

            public static PluginConfig DefaultConfig()
            {
                return new PluginConfig();
            }
        }

        private class UiSettings
        {
            public string ProfileCommand = "orpgxp";
            public string RageCommand = "orpgrage";
            public string UiCommand = "orpgui";

            public float AnchorMinX = 0.2f;
            public float AnchorMinY = 0.1f;
            public float AnchorMaxX = 0.8f;
            public float AnchorMaxY = 0.9f;
        }

        private class XpSettings
        {
            public double BaseKillNpc = 25;
            public double BaseKillPlayer = 50;
            public double BaseGatherOre = 2;
            public double BaseGatherWood = 1;
            public double BaseGatherPlants = 1.5;

            // Per-source multipliers so server owners can tune them
            public double BotReSpawnMultiplier = 1.0;
            public double NpcSpawnMultiplier = 1.0;
            public double ZombieHordeMultiplier = 1.0;

            public double LevelCurveBase = 100;    // XP for level 1→2
            public double LevelCurveGrowth = 1.25; // Each level multiplies required XP by this

            // Legacy per BotReSpawn profile multipliers (for migration)
            public Dictionary<string, double> BotReSpawnProfileMultipliers = new Dictionary<string, double>();

            // New full settings (per profile)
            public Dictionary<string, BotProfileXpSettings> BotReSpawnProfiles = new Dictionary<string, BotProfileXpSettings>();
        }

        private class RageSettings
        {
            public bool Enabled = true;

            // How many discipline points per character level
            public double CorePointsPerLevel = 1.0;

            // Fury shared settings
            public float FuryDurationSeconds = 10f;
            public float FuryMaxBonusDamage = 0.3f; // 30% at max Fury
            public float FuryOnKillGain = 0.15f;    // 15% fury per qualifying kill

            // Weapon specialization nodes – values here are per level
            const string RageIconBase = "https://raw.githubusercontent.com/jmg5215/OmniRPGEngine/main/assets/ui_images/Disciplines/RageNodeIcons/";
            const string RageTreeBase = RageIconBase + "tree/";

            public Dictionary<string, RageNodeConfig> Nodes = new Dictionary<string, RageNodeConfig>
            {
                // Tier 1 (core + weapon masteries)
                ["core_t1"] = new RageNodeConfig
                {
                    DisplayName = "Rage Core",
                    MaxLevel = 20,
                    DamageBonusPerLevel = 0.01f,
                    RecoilReductionPerLevel = 0.005f,
                    MoveSpeedPerLevel = 0.0025f,
                    Tier = 1,
                    TreeIconImage = RageTreeBase + "Core_Skills.png",
                    DetailIconImage = RageIconBase + "core/T1_Rage_Core.png",
                    ParentNodeId = null
                },
                ["rifle_t1"] = new RageNodeConfig
                {
                    DisplayName = "Predator's Focus",
                    MaxLevel = 10,
                    DamageBonusPerLevel = 0.02f,
                    CritChancePerLevel = 0.01f,
                    Tier = 1,
                    TreeIconImage = RageTreeBase + "Branch_Skills.png",
                    DetailIconImage = RageIconBase + "rifle/T2_Rage_Predators_Focus.png",
                    ParentNodeId = "core_t1"
                },
                ["shotgun_t1"] = new RageNodeConfig
                {
                    DisplayName = "Bonebreaker Load",
                    MaxLevel = 10,
                    DamageBonusPerLevel = 0.02f,
                    BleedChancePerLevel = 0.015f,
                    Tier = 1,
                    TreeIconImage = RageTreeBase + "Branch_Skills.png",
                    DetailIconImage = RageIconBase + "shotgun/T2_Rage_Bonebreaker_Load.png",
                    ParentNodeId = "core_t1"
                },
                ["pistol_t1"] = new RageNodeConfig
                {
                    DisplayName = "Quickkill Reflexes",
                    MaxLevel = 10,
                    DamageBonusPerLevel = 0.015f,
                    CritDamagePerLevel = 0.015f,
                    Tier = 1,
                    TreeIconImage = RageTreeBase + "Branch_Skills.png",
                    DetailIconImage = RageIconBase + "pistol/T2_Rage_Quickkill_Reflexes.png",
                    ParentNodeId = "core_t1"
                },

                // Tier 2 (branch outward)
                ["core_t2_bloodlust"] = new RageNodeConfig
                {
                    DisplayName = "Bloodlust",
                    MaxLevel = 5,
                    Tier = 2,
                    TreeIconImage = RageTreeBase + "Core_Skills.png",
                    DetailIconImage = RageIconBase + "core/T2_Rage_Core_Bloodlust.png",
                    ParentNodeId = "core_t1"
                },
                ["rifle_t2_predators_focus"] = new RageNodeConfig
                {
                    DisplayName = "Overcharge Rounds",
                    MaxLevel = 5,
                    Tier = 2,
                    TreeIconImage = RageTreeBase + "Branch_Skills.png",
                    DetailIconImage = RageIconBase + "rifle/T3_Rage_Overcharge_Rounds.png",
                    ParentNodeId = "rifle_t1"
                },
                ["shotgun_t2_bonebreaker"] = new RageNodeConfig
                {
                    DisplayName = "Sawed-Off Fury",
                    MaxLevel = 5,
                    Tier = 2,
                    TreeIconImage = RageTreeBase + "Branch_Skills.png",
                    DetailIconImage = RageIconBase + "shotgun/T3_Rage_SawedOff_Fury.png",
                    ParentNodeId = "shotgun_t1"
                },
                ["pistol_t2_quickkill_reflexes"] = new RageNodeConfig
                {
                    DisplayName = "Street Executioner",
                    MaxLevel = 5,
                    Tier = 2,
                    TreeIconImage = RageTreeBase + "Branch_Skills.png",
                    DetailIconImage = RageIconBase + "pistol/T3_Rage_Street_Executioner.png",
                    ParentNodeId = "pistol_t1"
                },

                // Tier 3 (outer ring)
                ["core_t3_berserker_instinct"] = new RageNodeConfig
                {
                    DisplayName = "Berserker Instinct",
                    MaxLevel = 5,
                    Tier = 3,
                    TreeIconImage = RageTreeBase + "Core_Skills.png",
                    DetailIconImage = RageIconBase + "core/T3_Rage_Core_Berserker_Instinct.png",
                    ParentNodeId = "core_t2_bloodlust"
                },
                ["rifle_t3_overcharge_rounds"] = new RageNodeConfig
                {
                    DisplayName = "Deathline Marksman",
                    MaxLevel = 5,
                    Tier = 3,
                    TreeIconImage = RageTreeBase + "Branch_Skills.png",
                    DetailIconImage = RageIconBase + "rifle/T4_Rage_Deathline_Marksman.png",
                    ParentNodeId = "rifle_t2_predators_focus"
                },
                ["shotgun_t3_sawedoff_fury"] = new RageNodeConfig
                {
                    DisplayName = "Shrapnel Storm",
                    MaxLevel = 5,
                    Tier = 3,
                    TreeIconImage = RageTreeBase + "Branch_Skills.png",
                    DetailIconImage = RageIconBase + "shotgun/T4_Rage_Shrapnel_Storm.png",
                    ParentNodeId = "shotgun_t2_bonebreaker"
                },
                ["pistol_t3_street_executioner"] = new RageNodeConfig
                {
                    DisplayName = "Gunslinger's Frenzy",
                    MaxLevel = 5,
                    Tier = 3,
                    TreeIconImage = RageTreeBase + "Branch_Skills.png",
                    DetailIconImage = RageIconBase + "pistol/T4_Rage_Gunslingers_Frenzy.png",
                    ParentNodeId = "pistol_t2_quickkill_reflexes"
                }
            };
            
            // NEW: respec settings
            public RespecSettings Respec = new RespecSettings();
        }

        private class RageNodeConfig
        {
            public string DisplayName = "Unnamed";
            public int MaxLevel = 10;

            public float DamageBonusPerLevel = 0f;
            public float CritChancePerLevel = 0f;
            public float CritDamagePerLevel = 0f;
            public float BleedChancePerLevel = 0f;
            public float MoveSpeedPerLevel = 0f;
            public float RecoilReductionPerLevel = 0f;

            public int Tier = 1;                 // 1 = inner ring, 2 = mid ring, 3 = outer ring
            public string TreeIconImage = null;   // small icon used on the skill tree nodes
            public string DetailIconImage = null; // large art shown on the right-hand description panel
            public string ParentNodeId = null;   // optional – who this node branches from
        }

        // NEW: RespecSettings definition
        private class RespecSettings
        {
            [JsonProperty("Respec Enabled")]
            public bool Enabled = true;

            // economics / serverrewards / item / none
            [JsonProperty("Respec Mode")]
            public string Mode = "economics";

            [JsonProperty("Economics Cost")]
            public double EconomicsCost = 0;

            [JsonProperty("ServerRewards Cost (RP)")]
            public int ServerRewardsCost = 0;

            [JsonProperty("Item Shortname")]
            public string ItemShortname = "scrap";

            [JsonProperty("Item Amount")]
            public int ItemAmount = 0;
        }

        private class BotProfileXpSettings
        {
            public double Multiplier = 1.0;
            public double FlatXp = 0.0;
        }

        protected override void LoadDefaultConfig()
        {
            config = PluginConfig.DefaultConfig();
            SaveConfig();
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<PluginConfig>();
                if (config == null)
                {
                    throw new Exception("Config file is null");
                }

                if (config.XP.BotReSpawnProfiles == null)
                    config.XP.BotReSpawnProfiles = new Dictionary<string, BotProfileXpSettings>();

                // Migration from legacy BotReSpawnProfileMultipliers if needed
                if (config.XP.BotReSpawnProfileMultipliers != null &&
                    config.XP.BotReSpawnProfileMultipliers.Count > 0 &&
                    (config.XP.BotReSpawnProfiles == null || config.XP.BotReSpawnProfiles.Count == 0))
                {
                    foreach (var kvp in config.XP.BotReSpawnProfileMultipliers)
                    {
                        config.XP.BotReSpawnProfiles[kvp.Key] = new BotProfileXpSettings
                        {
                            Multiplier = kvp.Value,
                            FlatXp = 0.0
                        };
                    }
                }
            }
            catch
            {
                PrintWarning("Config file is malformed or missing, creating new one.");
                LoadDefaultConfig();
            }
        }

        protected override void SaveConfig()
        {
            Config.WriteObject(config, true);
        }

        #endregion

        #region Data

        private DynamicConfigFile dataFile;
        private Dictionary<ulong, PlayerData> players = new Dictionary<ulong, PlayerData>();

        private class PlayerData
        {
            public ulong UserId;
            public string LastKnownName;

            public double TotalXp;
            public int Level;
            public double CurrentXp;      // XP accumulated since last level
            public double XpToNextLevel;

            public int UnspentDisciplinePoints;

            public RageData Rage = new RageData();
            public long PlayerKills;
            public long NpcKills;
            public long Deaths;
            public double TotalPlayTimeSeconds;
            [NonSerialized] public float SessionStartTime;

            public PlayerData()
            {
            }

            public PlayerData(ulong id, string name)
            {
                UserId = id;
                LastKnownName = name;
                Level = 1;
                XpToNextLevel = 100;
            }
        }

        private class RageData
        {
            public int TreeLevel;
            public int UnspentPoints;
            public Dictionary<string, int> NodeLevels = new Dictionary<string, int>();

            // Fury
            public float FuryAmount; // 0–1
            public double FuryExpireTimestamp;

            // UI helpers
            public string LastUpgradedNodeId;
            public double LastUpgradeFlashTime;
            public string SelectedNodeId;

            // Progress: highest unlocked Rage tier (1 = Tier 1 only, 2 = Tier 2 unlocked, etc.)
            public int MaxUnlockedTier = 1;

            public RageData()
            {
                FuryAmount = 0f;
                FuryExpireTimestamp = 0;
                LastUpgradedNodeId = null;
                LastUpgradeFlashTime = 0;
                SelectedNodeId = null;
                MaxUnlockedTier = 1;
            }
        }

        private void LoadData()
        {
            dataFile = Interface.Oxide.DataFileSystem.GetFile(config.DataFileName);
            try
            {
                var stored = dataFile.ReadObject<Dictionary<ulong, PlayerData>>();
                if (stored != null)
                {
                    players = stored;
                }
            }
            catch
            {
                PrintWarning("Data file corrupted or missing, starting fresh.");
                players = new Dictionary<ulong, PlayerData>();
            }
        }

        private void SaveData()
        {
            dataFile.WriteObject(players);
        }

        private PlayerData GetOrCreatePlayerData(BasePlayer player)
        {
            if (!IsHumanBasePlayer(player))
                return null;

            var id = player.userID;
            PlayerData data;
            if (!players.TryGetValue(id, out data))
            {
                data = new PlayerData(id, player.displayName);
                data.XpToNextLevel = GetRequiredXpForLevel(1);
                players[id] = data;
            }
            else
            {
                data.LastKnownName = player.displayName;
            }

            return data;
        }

        #endregion

        #region Oxide Hooks

        private void Init()
        {
            LoadConfig();
            LoadData();

            permission.RegisterPermission(PERM_USE, this);
            permission.RegisterPermission(PERM_ADMIN, this);

            cmd.AddChatCommand(config.UI.ProfileCommand, this, CmdProfile);
            cmd.AddChatCommand(config.UI.RageCommand, this, CmdRage);
            cmd.AddChatCommand(config.UI.UiCommand, this, CmdOpenUi);
            cmd.AddChatCommand("orpg", this, CmdOpenUi);
            cmd.AddChatCommand("orpgadmin", this, CmdAdminUi);

            cmd.AddConsoleCommand("omnirpg.ui", this, "CCmdOpenUi");
            cmd.AddConsoleCommand("omnirpg.rage.upgrade", this, "CCmdRageUpgrade");
            cmd.AddConsoleCommand("omnirpg.rage.inspect", this, "CCmdRageInspect");
            cmd.AddConsoleCommand("omnirpg.rage.respec", this, "CCmdRageRespec");

            // Admin config editor commands
            cmd.AddConsoleCommand("omnirpg.admin.adjust", this, "CCmdAdminAdjust");
            cmd.AddConsoleCommand("omnirpg.admin.toggle", this, "CCmdAdminToggle");
            cmd.AddConsoleCommand("omnirpg.admin.save", this, "CCmdAdminSave");

            // Bot XP editor commands
            cmd.AddConsoleCommand("omnirpg.botxp.mult", this, "CCmdBotXpMult");
            cmd.AddConsoleCommand("omnirpg.botxp.flat", this, "CCmdBotXpFlat");
            cmd.AddConsoleCommand("omnirpg.botxp.page", this, "CCmdBotXpPage");
            
            // Register images after server starts
            // (RegisterImages itself is invoked from OnServerInitialized)
        }

        private void RegisterImages()
        {
            if (ImageLibrary == null)
            {
                Puts("[OmniRPG] ImageLibrary is not loaded, cannot register images.");
                return;
            }

            // Use raw GitHub URL for the Disciplines background
            string url = "https://raw.githubusercontent.com/jmg5215/OmniRPGEngine/refs/heads/main/assets/ui_images/Disciplines/disciplines_bg.png";

            ImageLibrary.Call("AddImage", url, IMAGE_DISCIPLINES_BG, 0UL);
            Puts($"[OmniRPG] Queued Disciplines background image: {url}");
        }

        private void OnServerInitialized()
        {
            RegisterImages();
        }

        private void OnServerSave()
        {
            foreach (var bp in BasePlayer.activePlayerList)
            {
                var data = GetOrCreatePlayerData(bp);
                if (data != null)
                {
                    data.TotalPlayTimeSeconds += Time.realtimeSinceStartup - data.SessionStartTime;
                    data.SessionStartTime = Time.realtimeSinceStartup;
                }
            }
            SaveData();
        }

        private void Unload()
        {
            SaveData();
            foreach (var pl in BasePlayer.activePlayerList)
            {
                DestroyProfileUi(pl);
            }
        }

        private void OnPlayerInit(BasePlayer player)
        {
            var data = GetOrCreatePlayerData(player);
            if (data != null)
            {
                data.SessionStartTime = Time.realtimeSinceStartup;
            }
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            var data = GetOrCreatePlayerData(player);
            if (data != null)
            {
                data.TotalPlayTimeSeconds += Time.realtimeSinceStartup - data.SessionStartTime;
            }
            SaveData();
            DestroyProfileUi(player);
        }

        private void OnEntityDeath(BaseCombatEntity entity, HitInfo info)
        {
            if (entity == null || info == null) return;

            var killer = info.InitiatorPlayer;
            if (killer == null || !killer.IsConnected) return;
            if (!permission.UserHasPermission(killer.UserIDString, PERM_USE))
                return;

            // Identify victim types
            var victimPlayer = entity as BasePlayer;
            var victimNpc = entity as BaseNpc;

            // Skip BotReSpawn NPCs entirely — they are handled by OnBotReSpawnNPCKilled
            if (victimPlayer != null && IsBotReSpawnNpc(victimPlayer))
                return;

            var killerData = GetOrCreatePlayerData(killer);
            if (killerData == null) return;

            double xp = 0;

            if (victimPlayer != null)
            {
                if (victimPlayer == killer)
                {
                    // Suicide or self-inflicted, no XP
                    xp = 0;
                }
                else
                {
                    // Real player kill
                    xp = config.XP.BaseKillPlayer;
                    killerData.PlayerKills++;

                    var victimData = GetOrCreatePlayerData(victimPlayer);
                    if (victimData != null)
                        victimData.Deaths++;
                }
            }
            else if (victimNpc != null)
            {
                // Non-player NPCs (animals, etc.)
                xp = config.XP.BaseKillNpc;
                killerData.NpcKills++;
            }

            if (xp <= 0) return;

            AwardXp(killer, xp, "Kill");
            OnRageKillEvent(killer, entity);
            SaveData();
        }

        private void OnDispenserGather(ResourceDispenser dispenser, BaseEntity entity, Item item)
        {
            var player = entity as BasePlayer;
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PERM_USE)) return;
            if (!IsHumanBasePlayer(player)) return;

            double xp = 0;

            if (dispenser.gatherType == ResourceDispenser.GatherType.Ore)
            {
                xp = config.XP.BaseGatherOre * item.amount;
            }
            else if (dispenser.gatherType == ResourceDispenser.GatherType.Tree)
            {
                xp = config.XP.BaseGatherWood * item.amount;
            }

            if (xp > 0)
                AwardXp(player, xp, "Gather");
        }

        private void OnCollectiblePickup(Item item, BasePlayer player)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PERM_USE)) return;
            if (!IsHumanBasePlayer(player)) return;

            var def = item.info;
            if (def == null) return;

            double xp = 0;

            if (def.shortname.Contains("corn") || def.shortname.Contains("pumpkin") ||
                def.shortname.Contains("hemp") || def.shortname.Contains("mushroom"))
            {
                xp = config.XP.BaseGatherPlants * item.amount;
            }

            if (xp > 0)
                AwardXp(player, xp, "Plants");
        }

        #endregion

        #region BotReSpawn Integration

        // Called when a BotReSpawn NPC is spawned
        private void OnBotReSpawnNPCSpawned(ScientistNPC npc, string profilename, string group)
        {
            if (npc == null) return;

            if (!botReSpawnIds.Contains(npc.userID))
                botReSpawnIds.Add(npc.userID);

            // Ensure we have a config entry for this profile
            EnsureBotProfileSettings(profilename);
        }

        // Called when a BotReSpawn NPC is killed
        private void OnBotReSpawnNPCKilled(ScientistNPC npc, string profilename, string group, HitInfo info)
        {
            if (npc == null || info == null) return;

            botReSpawnIds.Add(npc.userID);

            var killer = info.InitiatorPlayer;
            if (killer == null || !killer.IsConnected) return;
            if (!permission.UserHasPermission(killer.UserIDString, PERM_USE)) return;
            if (!IsHumanBasePlayer(killer)) return;

            var killerData = GetOrCreatePlayerData(killer);
            if (killerData == null) return;

            killerData.NpcKills++;


            var settings = EnsureBotProfileSettings(profilename);

            // Base XP from global + profile multiplier
            var baseXp = config.XP.BaseKillNpc * config.XP.BotReSpawnMultiplier * settings.Multiplier;

            // Additive flat XP per your choice (B)
            var xp = baseXp + settings.FlatXp;

            AwardXp(killer, xp, $"BotReSpawn ({profilename})");
            OnRageKillEvent(killer, npc);
            SaveData();
        }

        #endregion

        #region External API (XPerience-Compatible)

        [HookMethod("GiveXPID")]
        public void GiveXPID(ulong playerId, double amount)
        {
            var player = BasePlayer.FindByID(playerId);
            if (player == null || amount == 0) return;
            AwardXp(player, amount, "API_ID");
        }

        [HookMethod("GiveXP")]
        public void GiveXP(BasePlayer player, double amount)
        {
            if (player == null || amount == 0) return;
            AwardXp(player, amount, "API");
        }

        [HookMethod("GiveXPBasic")]
        public void GiveXPBasic(BasePlayer player, double amount)
        {
            if (player == null || amount == 0) return;
            AwardXp(player, amount, "API_BASIC");
        }

        #endregion

        #region XP Core

        private double GetRequiredXpForLevel(int level)
        {
            return config.XP.LevelCurveBase * Math.Pow(config.XP.LevelCurveGrowth, level - 1);
        }

        private void AwardXp(BasePlayer player, double amount, string reason)
        {
            if (player == null || amount <= 0) return;
            if (!IsHumanBasePlayer(player)) return;

            var data = GetOrCreatePlayerData(player);
            if (data == null) return;

            data.TotalXp += amount;
            data.CurrentXp += amount;

            while (data.CurrentXp >= data.XpToNextLevel)
            {
                data.CurrentXp -= data.XpToNextLevel;
                data.Level++;

                data.XpToNextLevel = GetRequiredXpForLevel(data.Level);

                var gainedPoints = config.Rage.CorePointsPerLevel;
                data.UnspentDisciplinePoints += (int)Math.Round(gainedPoints);

                data.Rage.UnspentPoints++;

                player.ChatMessage(
                    $"<color=#ffb74d>[OmniRPG]</color> You reached <color=#ffeb3b>level {data.Level}</color>! " +
                    $"You gained <color=#b3e5fc>+{gainedPoints:0}</color> discipline point(s) and <color=#f48fb1>+1</color> Rage point.");
            }

            player.SendConsoleCommand("gametip.showtoast", 1, $"<size=14><color=#ffb74d>+{amount:0} XP</color> ({reason})</size>");

            SaveData();
        }

        #endregion

        #region Utility

        private string FormatTime(double seconds)
        {
            if (seconds <= 0) return "0m";
            var t = TimeSpan.FromSeconds(seconds);
            if (t.TotalHours >= 1)
                return $"{(int)t.TotalHours}h {t.Minutes}m";
            return $"{t.Minutes}m {t.Seconds}s";
        }

        // Basic SteamID64 sanity check – real players only
        private bool IsLikelyHumanSteamId(ulong id)
        {
            var idStr = id.ToString();
            if (idStr.Length != 17) return false;
            if (!idStr.StartsWith("7656")) return false;
            return true;
        }

        private bool IsBotReSpawnId(ulong id)
        {
            if (botReSpawnIds.Contains(id))
                return true;

            if (BotReSpawn != null)
            {
                try
                {
                    var result = BotReSpawn.Call("IsBotReSpawn", id);
                    if (result is bool b && b)
                        return true;
                }
                catch { }
            }

            return false;
        }

        private bool IsBotReSpawnNpc(BasePlayer player)
        {
            if (player == null) return false;

            // Ask BotReSpawn directly if this NPC is one of its bots
            if (BotReSpawn != null && player is NPCPlayer npc)
            {
                try
                {
                    var result = BotReSpawn.Call("IsBotReSpawn", npc);
                    if (result is bool b && b)
                        return true;
                }
                catch
                {
                    // ignore errors from BotReSpawn
                }
            }

            // Check our tracked BotReSpawn IDs
            if (botReSpawnIds.Contains(player.userID))
                return true;

            // Fallback: use the ID-based API
            return IsBotReSpawnId(player.userID);
        }

        private bool IsHumanBasePlayer(BasePlayer player)
        {
            if (player == null) return false;
            if (player.IsNpc) return false;
            if (IsBotReSpawnNpc(player)) return false;

            return IsLikelyHumanSteamId(player.userID);
        }

        private bool IsHumanPlayerData(PlayerData data)
        {
            if (data == null) return false;
            if (!IsLikelyHumanSteamId(data.UserId)) return false;
            if (IsBotReSpawnId(data.UserId)) return false;

            return true;
        }

        // Ensure a BotReSpawn profile has settings in config, return current settings
        private BotProfileXpSettings EnsureBotProfileSettings(string profileName)
        {
            if (string.IsNullOrEmpty(profileName))
                profileName = "UnnamedProfile";

            if (config.XP.BotReSpawnProfiles == null)
                config.XP.BotReSpawnProfiles = new Dictionary<string, BotProfileXpSettings>();

            BotProfileXpSettings settings;
            if (!config.XP.BotReSpawnProfiles.TryGetValue(profileName, out settings))
            {
                settings = new BotProfileXpSettings { Multiplier = 1.0, FlatXp = 0.0 };
                config.XP.BotReSpawnProfiles[profileName] = settings;
                SaveConfig();
            }

            return settings;
        }

        private int GetBotXpPage(BasePlayer player)
        {
            int page;
            if (!botXpPage.TryGetValue(player.userID, out page))
                page = 0;
            return page;
        }

        private void SetBotXpPage(BasePlayer player, int page)
        {
            botXpPage[player.userID] = page;
        }

        #endregion

        #region Rage Discipline

        private RageNodeConfig GetRageNodeConfig(string nodeId)
        {
            RageNodeConfig def;
            if (!config.Rage.Nodes.TryGetValue(nodeId, out def))
                return null;
            return def;
        }

        private int GetRageNodeLevel(PlayerData data, string nodeId)
        {
            int lvl;
            if (data.Rage.NodeLevels.TryGetValue(nodeId, out lvl))
                return lvl;
            return 0;
        }

        private string GetRageNodeDescription(string nodeId)
        {
            switch (nodeId)
            {
                // Tier 1
                case "core_t1": return "Rage Core: global bonuses to damage, recoil and movement.";
                case "rifle_t1": return "Predators Focus: improves rifle handling and headshot potential.";
                case "shotgun_t1": return "Bonebreaker Load: enhances close-range impact and bleed chance.";
                case "pistol_t1": return "Quickkill Reflexes: boosts precision and critical damage output.";
                // Tier 2
                case "core_t2_bloodlust": return "Bloodlust: feeds ferocity, paving the path to Berserker.";
                case "rifle_t2_predators_focus": return "Predator's Focus: unwavering aim in sustained engagements.";
                case "shotgun_t2_bonebreaker": return "Bonebreaker Load: pulverizes defenses at point blank.";
                case "pistol_t2_quickkill_reflexes": return "Quickkill Reflexes: rapid target acquisition and execution.";
                // Tier 3
                case "core_t3_berserker_instinct": return "Berserker Instinct: primal, explosive combat intuition.";
                case "rifle_t3_overcharge_rounds": return "Overcharge Rounds: destabilizing, amplified projectile impact.";
                case "shotgun_t3_sawedoff_fury": return "Sawed-Off Fury: brutal, uncompromising close-range carnage.";
                case "pistol_t3_street_executioner": return "Street Executioner: ruthless finishing efficiency.";
                default: return string.Empty;
            }
        }

        // Uses ImageLibrary (if present) to fetch a sprite ID for Rage node icons.
        // Configure images in ImageLibrary with these keys:
        //   omnirpg_rage_core, omnirpg_rage_rifle, omnirpg_rage_shotgun, omnirpg_rage_pistol
        private string GetRageNodeIconSprite(string nodeId)
        {
            // Legacy fallback only; primary icon is now RageNodeConfig.TreeIconImage
            if (ImageLibrary == null) return string.Empty;
            string key = null;
            switch (nodeId)
            {
                case "core_t1": key = "omnirpg_rage_core"; break;
                case "rifle_t1": key = "omnirpg_rage_rifle"; break;
                case "shotgun_t1": key = "omnirpg_rage_shotgun"; break;
                case "pistol_t1": key = "omnirpg_rage_pistol"; break;
            }
            if (string.IsNullOrEmpty(key)) return string.Empty;
            try
            {
                var result = ImageLibrary.Call("GetImage", key);
                if (result is string s && !string.IsNullOrEmpty(s)) return s;
            }
            catch { }
            return string.Empty;
        }

        private float GetRageDamageBonus(PlayerData data, Item weapon)
        {
            if (!config.Rage.Enabled) return 0f;
            if (data == null) return 0f;

            float bonus = 0f;

            var coreCfg = GetRageNodeConfig("core_t1");
            if (coreCfg != null)
            {
                var coreLvl = GetRageNodeLevel(data, "core_t1");
                bonus += coreCfg.DamageBonusPerLevel * coreLvl;
            }

            if (weapon != null)
            {
                var shortname = weapon.info.shortname;

                if (shortname.Contains("rifle"))
                {
                    var cfg = GetRageNodeConfig("rifle_t1");
                    if (cfg != null)
                    {
                        var lvl = GetRageNodeLevel(data, "rifle_t1");
                        bonus += cfg.DamageBonusPerLevel * lvl;
                    }
                }
                else if (shortname.Contains("shotgun"))
                {
                    var cfg = GetRageNodeConfig("shotgun_t1");
                    if (cfg != null)
                    {
                        var lvl = GetRageNodeLevel(data, "shotgun_t1");
                        bonus += cfg.DamageBonusPerLevel * lvl;
                    }
                }
                else if (shortname.Contains("pistol") || shortname.Contains("revolver") ||
                         shortname.Contains("nailgun"))
                {
                    var cfg = GetRageNodeConfig("pistol_t1");
                    if (cfg != null)
                    {
                        var lvl = GetRageNodeLevel(data, "pistol_t1");
                        bonus += cfg.DamageBonusPerLevel * lvl;
                    }
                }
            }

            if (data.Rage.FuryAmount > 0f && Time.realtimeSinceStartup < data.Rage.FuryExpireTimestamp)
            {
                bonus += config.Rage.FuryMaxBonusDamage * Mathf.Clamp01(data.Rage.FuryAmount);
            }

            return bonus;
        }

        private void OnRageKillEvent(BasePlayer killer, BaseCombatEntity victim)
        {
            if (!config.Rage.Enabled) return;
            if (!IsHumanBasePlayer(killer)) return;

            var data = GetOrCreatePlayerData(killer);
            if (data == null) return;

            data.Rage.FuryAmount = Mathf.Clamp01(data.Rage.FuryAmount + config.Rage.FuryOnKillGain);
            data.Rage.FuryExpireTimestamp = Time.realtimeSinceStartup + config.Rage.FuryDurationSeconds;

            killer.SendConsoleCommand("gametip.showtoast", 1,
                $"<size=14><color=#e57373>Fury</color> {data.Rage.FuryAmount * 100f:0}%</size>");
        }

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

            var attacker = info.InitiatorPlayer;
            if (attacker == null) return;
            if (!permission.UserHasPermission(attacker.UserIDString, PERM_USE)) return;
            if (!IsHumanBasePlayer(attacker)) return;

            var data = GetOrCreatePlayerData(attacker);
            if (data == null) return;

            var weapon = info.Weapon?.GetItem();
            var bonus = GetRageDamageBonus(data, weapon);
            if (bonus <= 0f) return;

            info.damageTypes.ScaleAll(1f + bonus);
        }

        private bool TryChargeRespecCost(BasePlayer player)
        {
            if (player == null)
                return false;

            var r = config.Rage.Respec;
            if (!r.Enabled || string.IsNullOrEmpty(r.Mode))
            {
                // Respec is effectively free / disabled cost
                return true;
            }

            string mode = r.Mode.ToLowerInvariant();

            if (mode == "economics")
            {
                if (Economics == null)
                {
                    player.ChatMessage("<color=#ffb74d>[OmniRPG]</color> Respec cost is set to Economics, but the Economics plugin is not loaded.");
                    return false;
                }

                double balance = 0;
                var result = Economics.Call("Balance", player.UserIDString);
                if (result is double d) balance = d;
                else if (result is float f) balance = f;
                else if (result is int i) balance = i;

                if (balance < r.EconomicsCost)
                {
                    player.ChatMessage($"<color=#ffb74d>[OmniRPG]</color> You need <color=#e57373>{r.EconomicsCost:0}</color> coins to respec.");
                    return false;
                }

                Economics.Call("Withdraw", player.UserIDString, r.EconomicsCost);
                player.ChatMessage($"<color=#ffb74d>[OmniRPG]</color> Spent <color=#e57373>{r.EconomicsCost:0}</color> coins to respec.");
                return true;
            }

            if (mode == "serverrewards" || mode == "rustrewards" || mode == "rp")
            {
                if (ServerRewards == null)
                {
                    player.ChatMessage("<color=#ffb74d>[OmniRPG]</color> Respec cost is set to ServerRewards, but that plugin is not loaded.");
                    return false;
                }

                int cost = r.ServerRewardsCost;
                if (cost <= 0)
                    return true;

                // ServerRewards API: bool TakePoints(BasePlayer player, int amount)
                bool success = false;
                var result = ServerRewards.Call("TakePoints", player, cost);
                if (result is bool b) success = b;

                if (!success)
                {
                    player.ChatMessage($"<color=#ffb74d>[OmniRPG]</color> You need <color=#e57373>{cost}</color> RP to respec.");
                    return false;
                }

                player.ChatMessage($"<color=#ffb74d>[OmniRPG]</color> Spent <color=#e57373>{cost}</color> RP to respec.");
                return true;
            }

            if (mode == "item")
            {
                if (string.IsNullOrEmpty(r.ItemShortname) || r.ItemAmount <= 0)
                    return true;

                var def = ItemManager.FindItemDefinition(r.ItemShortname);
                if (def == null)
                {
                    player.ChatMessage($"<color=#ffb74d>[OmniRPG]</color> Respec item shortname '<color=#e57373>{r.ItemShortname}</color>' is invalid.");
                    return false;
                }

                int have = player.inventory.GetAmount(def.itemid);
                if (have < r.ItemAmount)
                {
                    player.ChatMessage($"<color=#ffb74d>[OmniRPG]</color> You need <color=#e57373>{r.ItemAmount}</color> x <color=#e57373>{def.displayName.english}</color> to respec.");
                    return false;
                }

                player.inventory.Take(null, def.itemid, r.ItemAmount);
                try
                {
                    // Ensure the player's inventory is updated on the server/client
                    // Oxide v2 PlayerInventory.ServerUpdate requires a float delta parameter
                    player.inventory?.ServerUpdate(0f);
                }
                catch
                {
                    // Some server builds may not expose ServerUpdate; ignore if unavailable
                }

                player.ChatMessage($"<color=#ffb74d>[OmniRPG]</color> Spent <color=#e57373>{r.ItemAmount}</color> x <color=#e57373>{def.displayName.english}</color> to respec.");
                return true;
            }

            // Unknown mode or "none" -> treat as free
            return true;
        }
        #endregion

        #region Chat + Console Commands

        private bool HasPerm(BasePlayer player, string perm, bool notify = true)
        {
            if (player == null) return false;

            if (!permission.UserHasPermission(player.UserIDString, perm))
            {
                if (notify)
                    player.ChatMessage("<color=#ffb74d>[OmniRPG]</color> You don't have permission to use this command.");
                return false;
            }

            return true;
        }

        private void CmdProfile(BasePlayer player, string command, string[] args)
        {
            if (!HasPerm(player, PERM_USE)) return;

            var data = GetOrCreatePlayerData(player);
            if (data == null)
            {
                player.ChatMessage("<color=#ffb74d>[OmniRPG]</color> No profile data found. Reconnect if this persists.");
                return;
            }

            player.ChatMessage(
                $"<color=#ffb74d>[OmniRPG]</color> Level <color=#ffeb3b>{data.Level}</color>  " +
                $"XP <color=#b3e5fc>{data.CurrentXp:0}/{data.XpToNextLevel:0}</color>  " +
                $"Total XP <color=#b3e5fc>{data.TotalXp:0}</color>\n" +
                $"Discipline Points: <color=#f48fb1>{data.UnspentDisciplinePoints}</color>  " +
                $"Rage Points: <color=#e57373>{data.Rage.UnspentPoints}</color>  " +
                $"Fury: <color=#e57373>{data.Rage.FuryAmount * 100f:0}%</color>\n" +
                $"Player Kills: <color=#ffcc80>{data.PlayerKills}</color> | " +
                $"NPC Kills: <color=#ffcc80>{data.NpcKills}</color> | " +
                $"Deaths: <color=#ffcc80>{data.Deaths}</color>");
        }

        private void CmdRage(BasePlayer player, string command, string[] args)
        {
            if (!HasPerm(player, PERM_USE)) return;

            var data = GetOrCreatePlayerData(player);
            if (data == null) return;

            if (args == null || args.Length == 0)
            {
                ShowRageSummary(player, data);
                return;
            }

            var sub = args[0].ToLower();

            if (sub == "add" && args.Length >= 3)
            {
                if (!HasPerm(player, PERM_ADMIN)) return;

                var nodeId = args[1].ToLower();
                int points;
                if (!int.TryParse(args[2], out points) || points <= 0)
                {
                    player.ChatMessage("<color=#ffb74d>[OmniRPG]</color> Usage: /orpgrage add <node> <points>");
                    return;
                }

                AllocateRagePoints(player, data, nodeId, points);
            }
            else if (sub == "respec")
            {
                if (!HasPerm(player, PERM_ADMIN)) return;

                ResetRageTree(player, data);
            }
            else
            {
                ShowRageSummary(player, data);
            }
        }

        private void AllocateRagePoints(BasePlayer player, PlayerData data, string nodeId, int points)
        {
            RageNodeConfig cfg;
            if (!config.Rage.Nodes.TryGetValue(nodeId, out cfg))
            {
                player.ChatMessage($"<color=#ffb74d>[OmniRPG]</color> Unknown Rage node '{nodeId}'.");
                return;
            }

            int current = GetRageNodeLevel(data, nodeId);
            int maxAlloc = cfg.MaxLevel - current;
            if (maxAlloc <= 0)
            {
                player.ChatMessage(
                    $"<color=#ffb74d>[OmniRPG]</color> Node <color=#e57373>{cfg.DisplayName}</color> is already at max level.");
                return;
            }

            int spend = Math.Min(points, maxAlloc);
            if (spend > data.Rage.UnspentPoints)
            {
                player.ChatMessage(
                    $"<color=#ffb74d>[OmniRPG]</color> Not enough Rage points. You have <color=#e57373>{data.Rage.UnspentPoints}</color>.");
                return;
            }

            data.Rage.UnspentPoints -= spend;
            int newLevel = current + spend;
            data.Rage.NodeLevels[nodeId] = newLevel;

            // Record for tree flash highlight
            data.Rage.LastUpgradedNodeId = nodeId;
            data.Rage.LastUpgradeFlashTime = Time.realtimeSinceStartup;

            // Tier unlock: core node is the Tier 1 "Super Skill".
            // When it reaches max level, unlock Tier 2.
            if (nodeId.Equals("core_t1", StringComparison.OrdinalIgnoreCase) &&
                newLevel >= cfg.MaxLevel &&
                data.Rage.MaxUnlockedTier < 2)
            {
                data.Rage.MaxUnlockedTier = 2;
                player.ChatMessage("<color=#ffb74d>[OmniRPG]</color> Your mastery of Rage unlocks <color=#e57373>Tier 2</color>!");
            }

            player.ChatMessage(
                $"<color=#ffb74d>[OmniRPG]</color> Allocated <color=#e57373>{spend}</color> point(s) to " +
                $"<color=#e57373>{cfg.DisplayName}</color>. New level: <color=#e57373>{newLevel}/{cfg.MaxLevel}</color>. " +
                $"Remaining Rage points: <color=#e57373>{data.Rage.UnspentPoints}</color>");

            SaveData();
        }

        private void ResetRageTree(BasePlayer player, PlayerData data)
        {
            int spent = data.Rage.NodeLevels.Values.Sum();
            data.Rage.NodeLevels.Clear();
            data.Rage.UnspentPoints += spent;

            // Reset Rage tier progression and UI helpers
            data.Rage.MaxUnlockedTier = 1;
            data.Rage.LastUpgradedNodeId = null;
            data.Rage.LastUpgradeFlashTime = 0;
            data.Rage.SelectedNodeId = null;

            player.ChatMessage(
                $"<color=#ffb74d>[OmniRPG]</color> Rage tree reset. Refunded <color=#e57373>{spent}</color> points."
// ... [Code preview truncated due to length. Full source in attached file] ...

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