// Requires: UiEngineFramework using static Oxide.Plugins.UiEngineFramework; using static Oxide.Plugins.QualityCrafting; using static Oxide.Plugins.QualityCrafting.InventoryOverlay; using System.Collections.Generic; using Oxide.Core.Plugins; using UnityEngine; using Oxide.Game.Rust.Cui; using Newtonsoft.Json; using System.Data; using Oxide.Plugins.QualityCraftingExtensionMethods; using System.IO; using Oxide.Core; using Rust; using System; using Oxide.Core.Libraries.Covalence; using System.Linq; namespace Oxide.Plugins { [Info("QualityCrafting", "mr01sam", "3.0.3")] [Description("Level crafting skills to craft higher quaility items with better stats and features.")] public partial class QualityCrafting : CovalencePlugin { public static QualityCrafting INSTANCE; public static Configuration CONFIG => INSTANCE.config; [PluginReference] private Plugin QualityCraftingExtended; private static bool ExtendedIsLoaded => INSTANCE?.QualityCraftingExtended?.IsLoaded ?? false; public static UiEngineInstance SCOPE; public const string PermissionAdmin = "qualitycrafting.admin"; public bool PluginIsLoaded = false; public static Oxide.Core.VersionNumber? PluginVersion => INSTANCE?.Version; void Init() { INSTANCE = this; config.Version = INSTANCE.Version; if (UiEngineFramework.PluginVersion.HasValue) { var currentVersion = UiEngineFramework.PluginVersion.Value; var targetVersion = new VersionNumber(1, 0, 1); if (currentVersion < targetVersion) { PrintError($"[OUTDATED DEPENDENCY] - READ THIS!! This plugin requires UiEngineFramework v{targetVersion} or newer. You currently have v{currentVersion}. You MUST get the latest version of UiEngineFramework for this plugin to work properly."); } } SaveConfig(); } void OnServerInitialized() { SCOPE = UiEngine.Init(this); if (!permission.PermissionExists(PermissionAdmin)) { permission.RegisterPermission(PermissionAdmin, this); } AddCovalenceCommand(CONFIG.SkillsMenu.OpenMenuCommand, nameof(CmdSkills)); LoadAllData(); SyncPerksWithConfig(); // Data Tables.CreateDefaultCategoryLevelsTable(false); Tables.CreateDefaultItemLevelsTable(false); Tables.CreateDefaultQualityChancesTable(false); CraftingSkills.LoadXpRequirements(); // Images var images = new Dictionary { ["banner"] = config.InventoryOverlay.InspectionBannerImageUrl, ["star"] = config.QualityTiers.SingleStarImageUrl, ["star1"] = config.QualityTiers.ImageUrls[0], ["star2"] = config.QualityTiers.ImageUrls[1], ["star3"] = config.QualityTiers.ImageUrls[2], ["star4"] = config.QualityTiers.ImageUrls[3], ["star5"] = config.QualityTiers.ImageUrls[4], ["hudbutton.quality"] = CONFIG.HudButtons.QualityButton.IconUrl, ["hudbutton.skills"] = CONFIG.HudButtons.SkillsButton.IconUrl }; images.AddRange(config.Categories.ToDictionary(x => $"cat {x.Key.ToLower()}", x => x.Value.ImageUrl)); UiEngine.AddImages(images); Leaderboards.InitPlayerRankings(); UiEngine.LoadAddedImages(() => { // Load Players foreach (var basePlayer in BasePlayer.activePlayerList) { InitQualityCraftingPlayer(basePlayer); } }); foreach (var basePlayer in BasePlayer.activePlayerList) { InitQualityCraftingPlayer(basePlayer); } PluginIsLoaded = true; } public void LoadedExtensionPlugin() { SyncPerksWithConfig(); UiEngine.LoadAddedImages(); ClearCache(); foreach (var basePlayer in BasePlayer.activePlayerList) { InitQualityCraftingPlayer(basePlayer); } } void OnNewSave(string filename) { if (config.Options.ResetDataOnMapWipe) { BackupAndClearData(); } } void OnServerSave() { if (config.Options.SaveOnServerSave) { SaveAllData(); } } void Unload() { SaveAllData(); foreach (var player in BasePlayer.activePlayerList) { OnPlayerDisconnected(player); } RemoveAndDestroyAllPlayerTickBehaviors(); ClearCache(); SCOPE?.Dispose(); SCOPE = null; } void OnPlayerSleep(BasePlayer basePlayer) { if (basePlayer == null || !basePlayer.IsConnected || !basePlayer.IsRealPlayer()) { return; } Inspection.CloseInspection(basePlayer); SkillsMenu.Close(basePlayer); InventoryOverlay.DestroyAll(basePlayer); Tracking.Destroy(basePlayer); HudButtons.Close(basePlayer); } void OnPlayerSleepEnded(BasePlayer basePlayer) { if (basePlayer == null || !basePlayer.IsConnected || !basePlayer.IsRealPlayer()) { return; } timer.In(0.2f, () => { InitQualityCraftingPlayer(basePlayer); }); } private void InitQualityCraftingPlayer(BasePlayer basePlayer) { if (basePlayer == null || !basePlayer.IsConnected || !basePlayer.IsRealPlayer()) { return; } Tracking.Show(basePlayer, true); InventoryOverlay.UpdateAll(basePlayer, basePlayer.inventory.containerBelt); HudButtons.ShowQualityButton(basePlayer); HudButtons.ShowSkillsButton(basePlayer); if (_behaviours.ContainsKey(basePlayer.userID)) { return; } AddPlayerTickBehavior(basePlayer); } void OnPlayerDisconnected(BasePlayer basePlayer) { if (basePlayer == null) { return; } RemovePlayerTickBehavior(basePlayer.userID); OnPlayerSleep(basePlayer); } public void SaveAllData() { Tracking.SavePlayerTracking(); Leveling.SavePlayerCraftingSkills(); if (CONFIG.SkillsMenu.ShowStatistics) { Statistics.SaveStatistics(); } } public void LoadAllData() { Tracking.LoadPlayerTracking(); Leveling.LoadPlayerCraftingSkills(); if (CONFIG.SkillsMenu.ShowStatistics) { Statistics.LoadStatistics(); } } public void SyncPerksWithConfig() { var localization = new Dictionary(); // Add perks if they don't exist foreach (var perk in Perks.ALL.Values) { if (!config.Perks.ContainsKey(perk.ID)) { config.Perks.Add(perk.ID, new PerkConfig { ImageUrl = perk.ImageUrl, MaxRank = perk.MaxRank, Modifier = perk.Modifier, Weight = perk.Weight, Color = perk.Color }); } } SaveConfig(); // Update perk info from config foreach (var kvp in config.Perks) { if (Perks.ALL.ContainsKey(kvp.Key)) { var perk = Perks.ALL[kvp.Key]; var value = kvp.Value; perk.Color = value.Color; perk.ImageUrl = value.ImageUrl; perk.Modifier = value.Modifier; perk.Weight = value.Weight; perk.MaxRank = value.MaxRank; } } // Add images UiEngine.AddImages(Perks.ALL.Values.ToDictionary(x => x.IconID, x => x.ImageUrl)); // Calculate weights WeightedPerksPerCategory = new Dictionary>(); PerksForCategory = new Dictionary>(); foreach(var category in config.Categories) { var cat = Enum.Parse(category.Key); WeightedPerksPerCategory[cat] = new List(); PerksForCategory[cat] = new List(); localization[$"category {cat.ToString().ToLower()}"] = DefaultCategoryNames.GetValueOrDefault(cat); } foreach (var perk in Perks.ALL.Values) { foreach (var category in perk.Categories) { WeightedPerksPerCategory.GetValueOrNew(category).AddRange(Enumerable.Repeat(perk.ID, perk.Weight)); PerksForCategory.GetValueOrNew(category).Add(perk.ID); } } // Add localization foreach (var perk in Perks.ALL.Values) { localization[$"{perk.ID} name"] = perk.DefaultName; localization[$"{perk.ID} description"] = perk.DefaultDescription; } LoadAdditionalMessages(localization); } } } namespace Oxide.Plugins { public partial class QualityCrafting { } } namespace Oxide.Plugins { public partial class QualityCrafting { public interface IConfigExtension { } public class LegacyConfig { public VersionNumber Version { get; set; } = new VersionNumber(0, 0, 0); } public partial class Configuration { [ConfigDescription("Do not edit this.")] public VersionNumber Version = new VersionNumber(0, 0, 0); [ConfigDescription("")] public OptionsConfig Options = new OptionsConfig(); [ConfigDescription("")] public Dictionary Perks = new Dictionary(); [ConfigDescription("")] public Dictionary Categories = new Dictionary { [CraftingCategoryType.Clothing.ToString()] = new CategoryConfig { Enabled = true, ImageUrl = "https://i.ibb.co/9YhRZCH/tailoring.png" }, [CraftingCategoryType.Melee.ToString()] = new CategoryConfig { Enabled = true, ImageUrl = "https://i.ibb.co/7Gyb4X4/weaponsmithing.png" }, [CraftingCategoryType.Bows.ToString()] = new CategoryConfig { Enabled = true, ImageUrl = "https://i.ibb.co/x50T2yJ/bowmaking.png" }, [CraftingCategoryType.Firearms.ToString()] = new CategoryConfig { Enabled = true, ImageUrl = "https://i.ibb.co/rxpH6B3/gunsmithing.png" }, [CraftingCategoryType.Tools.ToString()] = new CategoryConfig { Enabled = true, ImageUrl = "https://i.ibb.co/t3gVHKv/toolcrafting.png" } }; [ConfigDescription("")] public LevelingConfig ItemLeveling = new LevelingConfig { Table = Tables.DEFAULT_ITEM_LEVELS_TABLE, XpGainedForItemWorkbenchTier = new int[] { 1, 1, 1, 1, 1 } }; [ConfigDescription("")] public CategoryLevelingConfig CategoryLeveling = new CategoryLevelingConfig { Table = Tables.DEFAULT_CATEGORY_LEVELS_TABLE, XpGainedForItemWorkbenchTier = new int[] { 1, 2, 3, 5, 8 }, DuplicationChancePerLevel = 0.001f, CraftingSpeedIncreasePerLevel = 0.01f }; [ConfigDescription("")] public QualityConfig QualityTiers = new QualityConfig { SingleStarImageUrl = "https://i.ibb.co/xLF7pk4/starb.png", ImageUrls = new string[] { "https://i.ibb.co/HdvFyvS/star1c.png", "https://i.ibb.co/Xyk7XdW/star2c.png", "https://i.ibb.co/PhpMr9t/star3c.png", "https://i.ibb.co/yNgYZW4/star4c.png", "https://i.ibb.co/N1RpDPV/star5c.png" }, RarityColors = new string[] { "1 1 1 1", "0.4 0.929 0.361 1", "0.2 0.459 0.91 1", "0.69 0.38 0.839 1", "1 0.839 0 1", "1 0.412 0 1" }, Table = Tables.DEFAULT_QUALITY_CHANCES_TABLE }; [ConfigDescription("")] public SkillsMenuConfig SkillsMenu = new SkillsMenuConfig(); [ConfigDescription("")] public TrackingConfig Tracking = new TrackingConfig(); [ConfigDescription("")] public InventoryOverlayConfig InventoryOverlay = new InventoryOverlayConfig(); [ConfigDescription("")] public HudButtonsConfig HudButtons = new HudButtonsConfig(); [ConfigDescription("")] public NotificationsConfig Notifications = new NotificationsConfig(); [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public object Extended = null; } public class OptionsConfig { [ConfigDescription("If true then perks will not appear on items that can't benefit from them. For example, Durable will not appear on items that do not have durability.")] public bool PreventUselessPerks { get; set; } = false; [ConfigDescription("If true then this plugin will save whenever your server is configured to save. Do NOT set this to true if you have your server save super frequently like every 1 minute. If you do this, there will be performance issues.")] public bool SaveOnServerSave { get; set; } = false; [ConfigDescription("If true then all player data will be automatically cleared when a new map is created or when the force wipe occurs.")] public bool ResetDataOnMapWipe { get; set; } = false; } public class QualityConfig { [ConfigDescription("Name of the table file for altering quality craft chances located within the oxide/data/QualityCrafting/v3 directory on your server.")] public string Table { get; set; } = ""; [ConfigDescription("URL image for a single quality star. These are the horizontal stars that appear in the inspection screen.")] public string SingleStarImageUrl { get; set; } [ConfigDescription("List of image URLs for each quality level 1-5. These are the stars that appear overlayed on items in the inventory.")] public string[] ImageUrls { get; set; } [ConfigDescription("List of colors for each quality level 1-5.")] public string[] RarityColors { get; set; } } public class CategoryConfig { [ConfigDescription("If true then this crafting category will be enabled. Otherwise it will be disabled for the features of this plugin.")] public bool Enabled = true; [ConfigDescription("The image URL for a category.")] public string ImageUrl = ""; [ConfigDescription("If true then this category will appear as tracked by default for new players.")] public bool TrackedByDefault = true; } public class PerkConfig { [ConfigDescription("The image that is displayed for a perk. Can be an asset sprite path or a URL to your own hosted image.")] public string ImageUrl = ""; [ConfigDescription("Color that corresponds to a perk.")] public string Color = "1 1 1 1"; [ConfigDescription("This property allows you to adjust how effective this perk is per rank. For example, if the perk is Lethal, and the Modifier is set to '20'. Then a weapon with that perk will do 20% more damage per Lethal rank. See the perk descriptions for what each rank increases.")] public float Modifier = 20; [ConfigDescription("This is the limit that a crafted item can naturally have for this perk. For example, if the perk is Durable, and MaxRank is set to 3, then a crafted item can never achieve a Durable rank higher than III. If its a 4 or 5 star item, it might have 3 ranks of Durable, but it will also have 2 ranks of some other perk(s). Perks applied manually through a command can bypass this limit.")] public int MaxRank = 3; [ConfigDescription("This will allow you to adjust how likely this perk is to appear on crafted items. It is relative to the other perk weights. So if all perks are set to Weight=1, then they will all be equally likely. If you were to then set one of those perks to Weight=2, then that perk will be twice as likely as the others. You can also set Weight=0 to make it so this perk will never appear on crafted items. It can still appear if this perk is manually applied through a command.")] public int Weight = 1; } public class NotificationsConfig { [ConfigDescription("If true then notifications will show when crafting quality items.")] public bool Enabled = true; [ConfigDescription("If true the notifications will play a sound.")] public bool PlaySfx = true; [ConfigDescription("The horizontal position. Negative is to the left, positive is to the right. Zero is is the CENTER of the screen.")] public int X = 24; [ConfigDescription("The vertical position. Negative is down, positive is up. Zero is is the BOTTOM of the screen.")] public int Y = 100; } public class InventoryOverlayConfig { [ConfigDescription("If true then clothing resistance modifiers will appear in the inventory overlay screen.")] public bool ShowClothingModifiers = true; [ConfigDescription("Allows you to adjust the scale size of the quality stars overlay on inventory items. Note that if this is too big then players will not be able to move items correctly.")] public float QualityStarsSizeScale = 1f; [ConfigDescription("The image URL for the background image under the item name on the inspection screen.")] public string InspectionBannerImageUrl = "https://i.ibb.co/N1THK27/banner-lines.png"; } public class TrackingConfig { [ConfigDescription("The max number of items or categories that a player can track at one time. Set to 0 to disable the tracking feature.")] public int MaxTrackedItems = 8; [ConfigDescription("The horizontal position. Negative is to the left, positive is to the right. Zero is is the CENTER of the screen.")] public int X = 310; [ConfigDescription("The vertical position. Negative is down, positive is up. Zero is is the BOTTOM of the screen.")] public int Y = 14; } public class HudButtonsConfig { [ConfigDescription("")] public SingleHudButtonConfig QualityButton = new SingleHudButtonConfig { IconUrl = SPRITE.Info, X = -240, Y = 24 }; [ConfigDescription("")] public SingleHudButtonConfig SkillsButton = new SingleHudButtonConfig { IconUrl = SPRITE.XP, X = 215, Y = 24 }; } public class SingleHudButtonConfig { [ConfigDescription("If true this button will be showned on the player HUD. Otherwise it will be hidden.")] public bool Show = true; [ConfigDescription("The sprite asset path or image URL for this button.")] public string IconUrl = ""; [ConfigDescription("The color of this button.")] public string Color = "1 1 1 0.5"; [ConfigDescription("If true this button will shown text underneath it. Otherwise there will be no text.")] public bool ShowText = true; [ConfigDescription("Font size of the text if shown.")] public int TextSize = 8; [ConfigDescription("Pixel width and height of the button.")] public int Size = 48; [ConfigDescription("The horizontal position. Negative is to the left, positive is to the right. Zero is is the CENTER of the screen.")] public int X = 0; [ConfigDescription("The vertical position. Negative is down, positive is up. Zero is is the BOTTOM of the screen.")] public int Y = 0; } #region Skills Menu public class SkillsMenuConfig { [ConfigDescription("The command players can use to open the skills menu in addition to the HUD button.")] public string OpenMenuCommand = "skills"; [ConfigDescription("If set to true then crafting statistics will be tracked for your players and displayed in their skill menu screen. Otherwise this area will be empty in the skills menu.")] public bool ShowStatistics = true; } #endregion #region Leveling public class LevelingConfig { [ConfigDescription("Name of the table file for the leveling curve located within the oxide/data/QualityCrafting/v3 directory on your server.")] public string Table = ""; [ConfigDescription("The amount of XP gained when crafting an item for each workbench tier 0-3. An item that does not require a workbench is the first number, and an item that requires a tier 3 workbench is the 4th number.")] public int[] XpGainedForItemWorkbenchTier = new[] { 1, 1, 1, 1 }; } public class CategoryLevelingConfig : LevelingConfig { [ConfigDescription("The chance a player has of duplicating an item they craft per category crafting level. For example, if set to 0.01, then this chance will increase by 1% for each category crafting level that player has.")] public float DuplicationChancePerLevel = 0f; [ConfigDescription("The speed increase of crafting per category crafting level. For example, if set to 0.1, then the speed will increase by 10% for each category crafting level that player has.")] public float CraftingSpeedIncreasePerLevel = 0.01f; } public LevelingConfig ItemLeveling = new LevelingConfig { XpGainedForItemWorkbenchTier = new int[] { 1, 1, 1, 1 } }; public CategoryLevelingConfig CategoryLeveling = new CategoryLevelingConfig { XpGainedForItemWorkbenchTier = new int[] { 1, 2, 3, 5 }, DuplicationChancePerLevel = 0.001f }; #endregion #region Boiler Plate Config public Configuration config; protected override void LoadConfig() { //base.LoadConfig(); //LoadDefaultConfig(); //return; LegacyConfig legacyConfig = null; try { var legacyConfigText = File.ReadAllText(Interface.Oxide.ConfigDirectory + @"\QualityCrafting.json"); legacyConfig = JsonConvert.DeserializeObject(legacyConfigText); } catch (Exception) { } if (legacyConfig != null && legacyConfig.Version.Major < 3 && legacyConfig.Version.Major > 0) { PrintWarning(new string('#', 20) + "[PLUGIN UPDATED] It seems you are upgrading from an older version of Quality Crafting. YOUR CONFIG HAS BEEN RESET. A lot of things have changed, so make sure to setup your config again! If you would like to import player skill data, see the plugin documentation page for instructions." + new string('#', 20)); base.LoadConfig(); LoadDefaultConfig(); SaveConfig(); } else { base.LoadConfig(); try { config = Config.ReadObject(); if (config == null) throw new Exception(); } catch { PrintError("Your configuration file contains an error. Using default configuration values."); LoadDefaultConfig(); } SaveConfig(); } } protected override void SaveConfig() => Config.WriteObject(config); protected override void LoadDefaultConfig() => config = new Configuration(); public void SaveConfigExternally() => SaveConfig(); #endregion } } namespace Oxide.Plugins.QualityCraftingExtensionMethods { public static class ExtensionMethods { public static bool WasThrown(this HitInfo info) => info.IsProjectile() && (info.WeaponPrefab is BaseMelee); public static BasePlayer BasePlayer(this IPlayer player) => player.Object as BasePlayer; public static string Lang(this BasePlayer basePlayer, string key, params object[] args) => QualityCrafting.Lang(basePlayer.UserIDString, key, args); public static string Lang(this IPlayer player, string key, params object[] args) => QualityCrafting.Lang(player.Id, key, args); public static void Chat(this IPlayer player, string key, params object[] args) => player.Message(player.Lang(key, args)); public static void Chat(this BasePlayer basePlayer, string key, params object[] args) => basePlayer.ChatMessage(basePlayer.Lang(key, args)); public static V GetValueOrNew(this Dictionary dict, K key) where V : new() => GetValueOrNew(dict, key, new V()); public static V GetValueOrNew(this Dictionary dict, K key, V v) where V : new() { var value = dict.GetValueOrDefault(key); if (value != null) { return value; } dict[key] = v != null ? v : new V(); return dict[key]; } public static T GetValueOrDefault(this ICollection list, int index) { if (index < list.Count) { return list.ElementAt(index); } return default(T); } public static void AddIfNotExists(this ICollection list, Func predicate, T value) { if (!list.Any(predicate)) { list.Add(value); } } public static T Random(this List list) { var idx = UnityEngine.Random.Range(0, list.Count - 1); return list[idx]; } public static List Extend(this List list, T extension) { list.Add(extension); return list; } public static List Extend(this List list, List extension) { list.AddRange(extension); return list; } public static ulong UserId(this IPlayer player) => ulong.Parse(player.Id); public static string Image(this CraftingCategoryType category) { return $"cat {category.ToString().ToLower()}"; } public static bool IsEnabled(this CraftingCategoryType category) => CONFIG.Categories.ContainsKey(category.ToString()) && CONFIG.Categories[category.ToString()].Enabled; public static string DisplayName(this CraftingCategoryType category, BasePlayer basePlayer) { return Oxide.Plugins.QualityCrafting.Lang(basePlayer.UserIDString, $"category {category.ToString().ToLower()}"); } public static CraftingSkills Skills(this BasePlayer basePlayer) => INSTANCE.PlayerCraftingSkills.GetValueOrNew(basePlayer.userID); public static CraftingSkills Skills(this IPlayer player) => INSTANCE.PlayerCraftingSkills.GetValueOrNew(player.UserId()); public static QualityItem? Quality(this Item item) { return QualityItem.Parse(item); } public static QualityItem? QualityIfExists(this Item item) { return item.IsQualityItem() ? QualityItem.Parse(item) : null; } public static bool IsQualityItem(this Item item) { //return item != null && !string.IsNullOrEmpty(item.text) && (item.IsBlueprint() || CraftingCategory.GetCraftingCategoryType(item) != CraftingCategoryType.None); return item != null && !string.IsNullOrWhiteSpace(item.text) && item.text.Split("|").Length > 1; } public static bool IsQualityCraftable(this Item item) { return CraftingCategory.GetCraftingCategoryType(item) != CraftingCategoryType.None; } public static void AddRange(this Dictionary dict, Dictionary otherDict) { foreach(var kvp in otherDict) { dict[kvp.Key] = kvp.Value; } } public static Item[] WornItems(this BasePlayer basePlayer) => basePlayer.inventory.containerWear.itemList.ToArray(); public static Item[] EquippedItems(this BasePlayer basePlayer) { var items = new List(); items.AddRange(basePlayer.inventory.containerWear.itemList); var active = basePlayer.GetActiveItem(); if (active != null) { items.Add(active); if (active.contents?.itemList != null) { items.AddRange(active.contents.itemList); } } return items.ToArray(); } public static float TryGet(this ProtectionProperties protectionProperties, Rust.DamageType damageType) { try { return protectionProperties.Get(damageType); } catch (Exception) { return 0f; } } public static float TryGet(this Rust.DamageTypeList damageTypes, Rust.DamageType damageType) { try { return damageTypes.Get(damageType); } catch (Exception) { return 0f; } } public static bool IsAssetPath(this string path) => path.StartsWith("assets/"); public static ClothingResistances Resistances(this BasePlayer basePlayer) { try { var resistances = new ClothingResistances(); foreach (var item in basePlayer.inventory.containerWear.itemList) { if (item.IsBackpack()) { continue; } foreach (var value in Enum.GetValues(typeof(Rust.DamageType))) { var dt = (Rust.DamageType)value; var ap = item.info.ItemModWearable.armorProperties; float res = item.info.ItemModWearable.protectionProperties.TryGet(dt); float bonus = 0f; var qi = item.QualityIfExists(); if (qi != null) { bonus = qi.ProtectionBonus.GetValueOrDefault(dt, 0f); if (bonus > 0f) { resistances.TypesWithBonuses.Add(dt); } else { resistances.TypesWithBonuses.Remove(dt); } } if (ap.Contains(HitArea.Head)) { resistances.Head[dt] = resistances.Head.GetValueOrDefault(dt) + res + bonus; } if (ap.Contains(HitArea.Leg)) { resistances.Legs[dt] = resistances.Legs.GetValueOrDefault(dt) + res + bonus; } if (ap.Contains(HitArea.Stomach)) { resistances.Body[dt] = resistances.Body.GetValueOrDefault(dt) + res + bonus; } } } return resistances; } catch (Exception e) { return new ClothingResistances(); } } public static float GetDamage(this AmmoTypes ammo) { switch (ammo) { case AmmoTypes.PISTOL_9MM: case AmmoTypes.RIFLE_556MM: case AmmoTypes.BOW_ARROW: return 50; case AmmoTypes.SHOTGUN_12GUAGE: return 210; case AmmoTypes.HANDMADE_SHELL: return 180; default: return 0f; } } public static bool IsPlayerContainer(this ItemContainer container, BasePlayer basePlayer) { var inventory = basePlayer.inventory; return container.uid == inventory.containerBelt.uid || container.uid == inventory.containerMain.uid || container.uid == inventory.containerWear.uid; } public static ItemContainer[] GetInventories(this BaseCombatEntity entity) { if (entity is StorageContainer storage) { return new ItemContainer[] { storage.inventory }; } if (entity is PlayerCorpse corpse) { return corpse.containers; } if (entity is DroppedItemContainer dropped) { return new ItemContainer[] { dropped.inventory }; } if (entity is BasePlayer basePlayer) { return new ItemContainer[] { basePlayer.inventory.containerBelt, basePlayer.inventory.containerMain, basePlayer.inventory.containerWear }; } if (entity is IndustrialCrafter crafter) { return new ItemContainer[] { crafter._inventory }; } return new ItemContainer[] { }; } public static ItemContainer GetInventory(this BaseCombatEntity entity) => GetInventories(entity).FirstOrDefault(); public static bool IsRealPlayer(this BasePlayer basePlayer) => !basePlayer.IsNpc && !basePlayer.IsBot; public static string Color(this string str, UiColor color) { return $"{str}"; } public static string Size(this string str, int fontSize) => $"{str}"; public static string FormatAs(this string str, params object[] args) => str.FormatAs(args.Select(x => x?.ToString()).ToArray()); public static string FormatAs(this string str, params string[] args) => string.Format(str, args); public static string Percent(this float value) => $"{(int)Math.Round(value * 100)}%"; public static int Floor(this decimal value) => (int)Math.Floor(value); public static int Floor(this double value) => (int)Math.Floor(value); public static int Floor(this float value) => (int)Math.Floor(value); public static int Round(this float value) => (int)Math.Round(value); public static ItemContainer GetItemContainer(this BasePlayer basePlayer, ulong uid, bool defaultMain = true) { if (basePlayer.inventory.containerBelt.uid.Value == uid) { return basePlayer.inventory.containerBelt; } if (basePlayer.inventory.containerMain.uid.Value == uid) { return basePlayer.inventory.containerMain; } if (basePlayer.inventory.containerWear.uid.Value == uid) { return basePlayer.inventory.containerWear; } if (defaultMain) { return basePlayer.inventory.containerMain; } return null; } public static bool HasItem(this BasePlayer basePlayer, Item item) { return basePlayer.inventory.AllItems().Any(x => x.uid == item.uid); } public static void GiveItemAt(this BasePlayer basePlayer, Item item, ItemContainer container, int position, bool backpack = false) { if (backpack) { var backpackItem = basePlayer.inventory.GetBackpackWithInventory(); if (backpackItem != null) { container = backpackItem.contents; } } if (!item.MoveToContainer(container, position) && !item.MoveToContainer(container) && !basePlayer.inventory.GiveItem(item)) { item.Drop(basePlayer.inventory.containerMain.dropPosition, basePlayer.inventory.containerMain.dropVelocity); } } public static bool HasItemAmount(this BasePlayer basePlayer, string itemShortName, int amount) { var items = basePlayer.inventory.AllItems(); var amountDue = amount; foreach (var item in items) { if (amountDue <= 0) { break; } if (item.info.shortname != itemShortName) { continue; } var amtToTake = item.amount; if (amountDue < amtToTake) { amtToTake = amountDue; } amountDue -= amtToTake; } return amountDue <= 0; } public static void TakeItem(this BasePlayer basePlayer, string itemShortName, int amount) { var items = basePlayer.inventory.AllItems(); var amountDue = amount; foreach ( var item in items ) { if (amountDue <= 0) { break; } if (item.info.shortname != itemShortName) { continue; } var amtToTake = item.amount; if (amountDue < amtToTake) { amtToTake = amountDue; } item.UseItem(amtToTake); amountDue -= amtToTake; } } public static List AllItems(this PlayerInventory playerInventory) { var items = new List(); playerInventory.GetAllItems(items); return items; } } } namespace Oxide.Plugins { public partial class QualityCrafting { public class LangDataEntry { public Dictionary Translated; } public static readonly Dictionary DefaultMessages = new Dictionary { ["layer head"] = "Head", ["layer body"] = "Body", ["layer legs"] = "Legs", ["tip"] = "Tip", ["tip1"] = "You can click on items or categories in the skill menu to track them on your HUD.", ["tip2"] = "Items that require higher workbench tiers grant more category XP.", ["tip3"] = "Quality of an item is represented by stars, higher quality items have more perks.", ["tip4"] = "Chances of crafting a higher quality is determined by item crafting level, not category.", ["tip5"] = "Category crafting level grants bonuses such as crafting speed and a chance to create duplicate items.", ["tip6"] = "Quality stars are shown in your inventory whenever you open a container or click the quality HUD button.", ["button skills"] = "Skills", ["button quality"] = "Quality", ["inspection perks"] = "Perks", ["inspection no perks"] = "This item does not have any perks.", ["inspection title"] = "Item Inspection", ["inspection stats"] = "Stats", ["inspection stat condition"] = "Condition", ["inspection stat damage scale"] = "Damage Scale", ["inspection stat damage"] = "Damage", ["inspection stat protection"] = "Protection", ["inspection stat bullet"] = "Bullet", ["inspection stat stab"] = "Stab", ["inspection stat explosion"] = "Explosion", ["inspection stat freezing"] = "Freezing", ["inspection stat radiation"] = "Radiation", ["inspection stat bite"] = "Bite", ["inspection stat slash"] = "Slash", ["inspection stat fall"] = "Fall", ["inspection stat blunt"] = "Blunt", ["inspection stat electric"] = "Electric", ["inspection stat gathering"] = "Gathering", ["inspection stat wood"] = "Wood", ["inspection stat ore"] = "Ore", ["inspection stat flesh"] = "Flesh", ["menu no statistics to show"] = "No statistics to show.", ["menu bonuses"] = "Bonuses", ["menu crafting speed"] = "Crafting Speed", ["menu duplicate chance"] = "Duplicate Chance", ["menu refining quality"] = "Refining Quality", ["menu no items crafted"] = "You have not crafted any items in this category yet.", ["menu item"] = "Item", ["menu tracking"] = "Tracking", ["menu crafting skills"] = "Crafting Skills", ["menu statistics"] = "Statistics", ["stats items crafted"] = "Items Crafted", ["stats quality percent"] = "Quality Percent", ["stats crafting rank"] = "Crafting Rank", ["stats items duplicated"] = "Items Duplicated", ["stats avg craft time"] = "Avg Craft Time", ["stats cloth consumed"] = "Cloth Consumed", ["stats springs consumed"] = "Springs Consumed", ["stats headshots"] = "Headshots", ["stats metal blades consumed"] = "Metal Blades Consumed", ["stats damage dealt"] = "Damage Dealt", ["stats resources gained"] = "Resources gained", ["stats rope consumed"] = "Rope Consumed", ["stats longest hit"] = "Longest Hit", ["stats damage taken"] = "Damage Taken", ["stats times refined"] = "Times Refined", ["notification crafted"] = "Crafted", ["notification duplicated"] = "Duplicated", ["tracking crafting skills"] = "Crafting Skills", ["xp"] = "XP", ["sec"] = "sec", ["defect"] = "DEFECT" }; protected readonly Dictionary> langFiles = new Dictionary>(); protected Dictionary LoadLocalizationFromFile(string lang = "en") { var file = GetLangFile(Name, lang); if (file == null) { SaveLangFile(DefaultMessages, this, lang); file = GetLangFile(Name, lang); langFiles[lang] = file; } else { foreach(var kvp in DefaultMessages) { if (!file.ContainsKey(kvp.Key)) { file[kvp.Key] = kvp.Value; } } SaveLangFile(file, this, lang); langFiles[lang] = file; } return file; } public static string Lang(string userIdString, string key, params object[] args) { var playerLanguage = INSTANCE.lang.GetLanguage(userIdString); if (!INSTANCE.langFiles.ContainsKey(playerLanguage)) { INSTANCE.LoadLocalizationFromFile(playerLanguage); } var file = INSTANCE.langFiles.GetValueOrDefault(playerLanguage); if (file == null) { return key; } return string.Format(INSTANCE.langFiles[playerLanguage].GetValueOrDefault(key, key), args); //return string.Format(INSTANCE.lang.GetMessage(key, INSTANCE, userIdString), args); } #region Built-In private Dictionary GetLangFile(string plugin, string lang = "en") { if (string.IsNullOrEmpty(plugin)) { return null; } char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); foreach (char oldChar in invalidFileNameChars) { lang = lang.Replace(oldChar, '_'); } string path = $"{lang}{Path.DirectorySeparatorChar}{plugin}.json"; string path2 = Path.Combine(Interface.Oxide.LangDirectory, path); if (!File.Exists(path2)) { return null; } return JsonConvert.DeserializeObject>(File.ReadAllText(path2)); } public void SaveLangFile(Dictionary messages, Plugin plugin, string lang = "en") { if (messages == null || string.IsNullOrEmpty(lang) || plugin == null) { return; } if (!Directory.Exists(Path.Combine(Interface.Oxide.LangDirectory, lang))) { Directory.CreateDirectory(Path.Combine(Interface.Oxide.LangDirectory, lang)); } File.WriteAllText(Path.Combine(Interface.Oxide.LangDirectory, $"{lang}/{Name}.json"), JsonConvert.SerializeObject(messages, Formatting.Indented)); } #endregion protected override void LoadDefaultMessages() { LoadLocalizationFromFile("en"); } public void LoadAdditionalMessages(Dictionary newMessages) { DefaultMessages.AddRange(newMessages); LoadLocalizationFromFile("en"); } } } namespace Oxide.Plugins { public partial class QualityCrafting { public static void Debug(string message) { INSTANCE.Puts(message); } public static IPlayer FindPlayer(ulong userId) { return INSTANCE.covalence.Players.FindPlayerById(userId.ToString()); } public static IPlayer FindPlayer(string userIdOrName) { return INSTANCE.covalence.Players.FindPlayer(userIdOrName); } public static List FindPlayers(IEnumerable userIds) { return userIds.Select(x => FindPlayer(x)).Where(x => x != null).ToList(); } private static float ScaleDamageTypeValue(Rust.DamageType damageType, float value, bool inverse = false) { switch (damageType) { case Rust.DamageType.ColdExposure: case Rust.DamageType.RadiationExposure: case Rust.DamageType.Bite: case Rust.DamageType.Explosion: value = inverse ? value / Const.ResistanceRatio : value * Const.ResistanceRatio; break; default: break; } return Math.Min(0.99f, value); } private static List Combine(params T[][] arrs) { var combined = new List(); foreach(var arr in arrs) { if (arr == null) { continue; } combined.AddRange(arr); } return combined; } private static string FormatStatWithModifier(int baseValue, int mod, string format="{0} ({1})", string singleFormat = "{0}") { if (mod == 0) { return string.Format(singleFormat, baseValue); } var color = mod > 0 ? Const.Colors.PositiveHighlight : Const.Colors.NegativeHighlight; var symbol = mod > 0 ? "+" : "-"; return string.Format(format, baseValue, (symbol + mod).Color(color)); } private static string FormatNumberWithSuffix(int number) { if (number >= 11 && number <= 13) { return number + "th"; } int lastDigit = number % 10; string suffix; switch (lastDigit) { case 1: suffix = "st"; break; case 2: suffix = "nd"; break; case 3: suffix = "rd"; break; default: suffix = "th"; break; } return number + suffix; } } } namespace Oxide.Plugins { public partial class QualityCrafting { public static readonly Dictionary CachedQualityItems = new Dictionary(); public void ClearCache() { foreach(var qi in CachedQualityItems.Values) { foreach (var perk in qi.Perks) { perk.Perk?.OnItemUnloaded?.Invoke(perk, qi); } } CachedQualityItems.Clear(); } } } namespace Oxide.Plugins { public partial class QualityCrafting { public static class Const { public static class Colors { public static readonly UiColor PositiveHighlight = COLOR.Rust.Lime; public static readonly UiColor NegativeHighlight = COLOR.Rust.Red; public static readonly string PanelSprite = "assets/content/ui/ui.background.tiletex.psd"; //public static readonly UiColor PanelColor = Color(0.969f, 0.922f, 0.882f, 0.2f); public static readonly UiColor PanelColor = Color(0.439f, 0.422f, 0.382f, 0.15f); public static readonly string PanelMaterial = MATERIAL.UIBackgroundBlur; public static readonly UiColor ButtonGreenColor = Color(0.45098f, 0.55294f, 0.27059f, 1f); public static readonly UiColor ButtonColor = COLOR.Rust.Blue; public static readonly UiColor ButtonTextColor = COLOR.Rust.LightBlue; public static readonly UiColor ButtonUnselectedColor = COLOR.Rust.Gray; public static readonly UiColor ButtonUnselectedTextColor = COLOR.Rust.LightGray; } public static Rust.DamageType[] _allDamageTypes = null; public static readonly UiColor StarColor = Color(1f, 0.8f, 0f, 1f); public const float ResistanceRatio = 1 / 6f; public static Rust.DamageType[] AllDamageTypes { get { if (_allDamageTypes == null) { _allDamageTypes = (Rust.DamageType[])Enum.GetValues(typeof(Rust.DamageType)); } return _allDamageTypes; } } public static ResourceDispenser.GatherType[] AllGatherTypes = new ResourceDispenser.GatherType[] { ResourceDispenser.GatherType.Ore, ResourceDispenser.GatherType.Tree, ResourceDispenser.GatherType.Flesh }; public static readonly List Tips = new List { "tip1", "tip2", "tip3", "tip4", "tip5", "tip6", }; } } } namespace Oxide.Plugins { public partial class QualityCrafting { public static class Perks { public static readonly PerkDefinition Lethal = new PerkDefinition { ID = "lethal", ImageUrl = "https://i.ibb.co/pKdCMGw/lethal.png", DefaultName = "Lethal", DefaultDescription = "Deals increased damage.", Color = "0.49 0.161 0.161 1", Weight = 5, Modifier = 20f, Categories = { CraftingCategoryType.Melee, CraftingCategoryType.Firearms, CraftingCategoryType.Bows, CraftingCategoryType.Tools }, OnItemLoaded = (itemPerk, qi) => { qi.DamageScaleBonus = itemPerk.Bonus; } }; public static readonly PerkDefinition Efficient = new PerkDefinition { ID = "efficient", ImageUrl = "https://i.ibb.co/ZHgwYg0/efficient.png", DefaultName = "Efficient", DefaultDescription = "Extracts more resources per hit from nodes.", Weight = 5, Modifier = 20f, Color = "0.369 0.62 0.361 1", Categories = { CraftingCategoryType.Melee, CraftingCategoryType.Tools }, IsPerkUseful = (item) => { var casted = item.GetHeldEntity() as BaseMelee; if (casted == null) { return false; } return casted.gathering.Flesh.gatherDamage > 0 || casted.gathering.Ore.gatherDamage > 0 || casted.gathering.Tree.gatherDamage > 0; }, OnItemLoaded = (itemPerk, qi) => { qi.GatherScaleBonus = itemPerk.Bonus; var casted = qi?.Item?.GetHeldEntity() as BaseMelee; if (casted == null) { return; } foreach(var gatherType in Const.AllGatherTypes) { var baseAmount = qi.BaseGather(gatherType); casted.gathering.GetFromIndex(gatherType).gatherDamage = baseAmount + (baseAmount * itemPerk.Bonus); } }, OnItemUnloaded = (itemPerk, qi) => { qi.GatherScaleBonus = 0f; var casted = qi?.Item?.GetHeldEntity() as BaseMelee; if (casted == null) { return; } foreach (var gatherType in Const.AllGatherTypes) { var baseAmount = qi.BaseGather(gatherType); casted.gathering.GetFromIndex(gatherType).gatherDamage = baseAmount; } } }; public static readonly PerkDefinition Durable = new PerkDefinition { ID = "durable", ImageUrl = "https://i.ibb.co/7RsnSBj/durable.png", DefaultName = "Durable", DefaultDescription = "Higher maximum condition.", Weight = 5, Modifier = 20f, Color = "0.659 0.42 0.161 1", Categories = { CraftingCategoryType.Melee, CraftingCategoryType.Firearms, CraftingCategoryType.Bows, CraftingCategoryType.Tools, CraftingCategoryType.Clothing }, IsPerkUseful = (item) => { return item.hasCondition; }, OnItemLoaded = (itemPerk, qi) => { var newItem = ItemManager.CreateByItemID(qi.Item.info.itemid); var category = qi.Category; var ratio = qi.Item._condition / qi.Item._maxCondition; qi.Item._maxCondition = newItem._maxCondition * itemPerk.Multiplier; qi.Item._condition = qi.Item._maxCondition * ratio; newItem.Remove(); }, OnItemUnloaded = (itemPerk, qi) => { var newItem = ItemManager.CreateByItemID(qi.Item.info.itemid); var category = qi.Category; var ratio = qi.Item._condition / qi.Item._maxCondition; qi.Item._maxCondition = newItem._maxCondition; qi.Item._condition = newItem._condition * ratio; newItem.Remove(); } }; public static readonly PerkDefinition Plated = new PerkDefinition { ID = "plated", ImageUrl = "https://i.ibb.co/fNXv4Jx/plated.png", DefaultName = "Plated", DefaultDescription = "Increased physical damage resistance.", Weight = 5, Modifier = 10f, Color = "0.541 0.678 0.702 1", Categories = { CraftingCategoryType.Clothing }, IsPerkUseful = (item) => { return item.info.isWearable; }, OnItemLoaded = (itemPerk, qi) => { var amount = itemPerk.Rank * (itemPerk.Perk.Modifier / 100); foreach(var dt in new Rust.DamageType[] { Rust.DamageType.Bullet, Rust.DamageType.Stab, Rust.DamageType.Bite, Rust.DamageType.Slash }) { qi.ProtectionBonus[dt] = amount + qi.ProtectionBonus.GetValueOrDefault(dt); } } }; public static Dictionary ALL = new Dictionary { [Lethal.ID] = Lethal, [Durable.ID] = Durable, [Efficient.ID] = Efficient, [Plated.ID] = Plated }; } } } namespace Oxide.Plugins { public partial class QualityCrafting { // For generating a file with config descriptions, not for gameplay use [Command("qc.printconfig")] private void PrintConfigReadme(IPlayer player, string command, string[] args) { if (!player.IsServer) { return; } ConfigPrinter.PrintDescriptions(INSTANCE); Puts("Printed config readme"); } #region Generic [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct | System.AttributeTargets.Property | System.AttributeTargets.Field)] public class ConfigDescriptionAttribute : System.Attribute { public readonly string Text; public ConfigDescriptionAttribute(string text) { Text = text; } } public static class ConfigPrinter { private struct DescriptionInfo { public string Label; public string Value; public int Depth; } public static void PrintDescriptions(CovalencePlugin plugin) { var descriptions = CollectConfigDescriptions(); var path = Interface.Oxide.DataDirectory + $"/{plugin.Name}/docs/"; Directory.CreateDirectory(path); // Formatting for Google Docs File.WriteAllText(path + "configreadme.txt", string.Join("\n\n", descriptions.Select(x => $"{new string(' ', x.Depth*2)}* **{x.Label}**: {(string.IsNullOrWhiteSpace(x.Value) ? ("") : ($"\n\n {new string(' ', (x.Depth+1)*2)}* {x.Value}"))}"))); } private static List CollectConfigDescriptions() { List descriptions = new List(); CollectDescriptionsRecursive(typeof(T), descriptions, 0); return descriptions; } private static void CollectDescriptionsRecursive(Type type, List descriptions, int depth) { // Handle properties System.Reflection.PropertyInfo[] properties = type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); foreach (var property in properties) { var attribute = property.GetCustomAttributes(false).FirstOrDefault(x => x is ConfigDescriptionAttribute) as ConfigDescriptionAttribute; ProcessMember(property, property.PropertyType, attribute, property.GetValue, descriptions, depth); } // Handle fields System.Reflection.FieldInfo[] fields = type.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); foreach (System.Reflection.FieldInfo field in fields) { var attribute = field.GetCustomAttributes(false).FirstOrDefault(x => x is ConfigDescriptionAttribute) as ConfigDescriptionAttribute; ProcessMember(field, field.FieldType, attribute, field.GetValue, descriptions, depth); } } private static void ProcessMember(System.Reflection.MemberInfo member, Type memberType, ConfigDescriptionAttribute attribute, Func getValue, List descriptions, int depth) { if (attribute != null) { descriptions.Add(new DescriptionInfo { Label = member.Name, Value = attribute.Text, Depth = depth }); } var nextDepth = depth + 1; if (memberType.IsClass && memberType != typeof(string)) { if (memberType.IsGenericType && memberType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { // Get the type of the values in the dictionary Type valueType = memberType.GetGenericArguments()[1]; CollectDescriptionsRecursive(valueType, descriptions, nextDepth); } else { CollectDescriptionsRecursive(memberType, descriptions, nextDepth); } } } } #endregion } } namespace Oxide.Plugins { public partial class QualityCrafting { #region Crafting void OnItemCraft(ItemCraftTask task, BasePlayer basePlayer, Item item) { if (!PluginIsLoaded) { return; } OnQualtyItemCraftStart(task, basePlayer); } void OnItemCraftFinished(ItemCraftTask task, Item item, ItemCrafter crafter) { if (!PluginIsLoaded) { return; } OnQualityItemCrafted(crafter.baseEntity, item, task); } #endregion #region Overlays void OnPlayerDeath(BasePlayer basePlayer, HitInfo info) { if (!PluginIsLoaded) { return; } if (basePlayer == null) { return; } OnPlayerSleep(basePlayer); } void OnLootEntity(BasePlayer basePlayer, BaseCombatEntity entity) { if (!PluginIsLoaded) { return; } if (basePlayer == null || entity == null) { return; } if (entity is VendingMachine casted) { InventoryOverlay.SetVendingMachineMode(basePlayer, casted.CanPlayerAdmin(basePlayer) ? InventoryOverlay.VendingMachineMode.Admin : InventoryOverlay.VendingMachineMode.Shop); } InventoryOverlay.SetViewingEntity(basePlayer, entity); timer.In(0.15f, () => { if (basePlayer == null || entity == null) { return; } foreach (var inventory in entity.GetInventories()) { InventoryOverlay.UpdateAll(basePlayer, inventory, true); } }); } void OnLootEntityEnd(BasePlayer basePlayer, BaseCombatEntity entity) { if (!PluginIsLoaded) { return; } if (basePlayer == null || entity == null) { return; } InventoryOverlay.SetViewingEntity(basePlayer, null); InventoryOverlay.DestroyAllButHotbar(basePlayer); } public struct RepairBenchInstance { public QualityItem CurrentItem; } public Dictionary RepairBenchInstances = new Dictionary(); void OnItemRemove(Item item) { // RepairBench Skinning if (!PluginIsLoaded) { return; } if (item == null) { return; } var container = item.GetRootContainer(); if (container == null) { return; } var repairBench = container.entityOwner as RepairBench; if (repairBench == null) { return; } var entityId = repairBench.net.ID.Value; var rbi = RepairBenchInstances.GetValueOrNew(entityId); rbi.CurrentItem = item.QualityIfExists(); RepairBenchInstances[entityId] = rbi; NextTick(() => { if (repairBench == null || container == null) { return; } var rbi = RepairBenchInstances.GetValueOrNew(entityId); if (rbi.CurrentItem == null) { return; } var qi = rbi.CurrentItem; if (qi?.Quality <= 0) { return; } var currentItemInRepairBench = container.itemList.FirstOrDefault(); if (currentItemInRepairBench == null) { return; } qi.Duplicate(currentItemInRepairBench); rbi.CurrentItem = currentItemInRepairBench.QualityIfExists(); RepairBenchInstances[entityId] = rbi; }); } void OnItemRemovedFromContainer(ItemContainer container, Item item) { if (!PluginIsLoaded) { return; } if (container != null && container.playerOwner != null && container.entityOwner != null && container.entityOwner.name == container.playerOwner.displayName && RepairBenchInstances.ContainsKey(container.entityOwner.net.ID.Value)) { NextTick(() => { // Skinner compatability var entity = container.entityOwner; if (entity == null) { return; } var entityId = entity.net.ID.Value; if (!RepairBenchInstances.ContainsKey(entityId)) { return; } var rbi = RepairBenchInstances.GetValueOrNew(entityId); var qi = rbi.CurrentItem; if (qi == null) { return; } qi.Duplicate(item); NextTick(() => { if (item == null) { return; } var basePlayer = item.GetOwnerPlayer(); if (basePlayer == null) { return; } var container = item.GetRootContainer(); if (container == null) { return; } InventoryOverlay.UpdateAt(basePlayer, container, item.position, true); var oldContainer = InventoryOverlay.GetViewingEntity(basePlayer)?.GetInventory(); if (oldContainer != null) { InventoryOverlay.UpdateAt(basePlayer, oldContainer, 0, true); } RepairBenchInstances.Remove(entityId); }); }); } else if (container.playerOwner != null) { // Item moved from player inventory to another source var basePlayer = container.playerOwner; if (!container.IsPlayerContainer(basePlayer)) { return; } InventoryOverlay.UpdateAt(basePlayer, container, item.position, false); timer.In(0.1f, () => { if (item == null) { return; } var newContainer = item.GetRootContainer(); if (basePlayer == null || newContainer == null) { return; } Interface.CallHook("QC_ItemMovedToContainer", basePlayer, newContainer, item); }); } } void OnItemAddedToContainer(ItemContainer container, Item item) { if (!PluginIsLoaded) { return; } if (container == null) { return; } if (container.playerOwner != null && container.playerOwner.IsRealPlayer() && container.entityOwner != null && container.entityOwner.name == container.playerOwner.displayName) { // Skinner compatability var entity = container.entityOwner; var entityId = entity.net.ID.Value; var rbi = RepairBenchInstances.GetValueOrNew(entityId); rbi.CurrentItem = item.QualityIfExists(); RepairBenchInstances[entityId] = rbi; InventoryOverlay.SetViewingEntity(container.playerOwner, container.entityOwner as BaseCombatEntity); } } void OnPlayerInput(BasePlayer basePlayer, InputState input) { if (!PluginIsLoaded) { return; } if (basePlayer == null) { return; } if (input.current.mouseDelta != input.previous.mouseDelta && InventoryOverlay.HasAnyOverlayExceptHotbar(basePlayer)) { InventoryOverlay.DestroyAllButHotbar(basePlayer); } } void OnItemPickup(Item item, BasePlayer basePlayer) { if (!PluginIsLoaded) { return; } if (basePlayer == null || !basePlayer.IsRealPlayer()) { return; } NextTick(() => { var container = item.GetRootContainer(); if (container == null || basePlayer == null) { return; } if (!container.IsPlayerContainer(basePlayer)) { return; } InventoryOverlay.UpdateAt(basePlayer, container, item.position, false); }); } void CanMoveItem(Item item, PlayerInventory playerLoot, ItemContainerId targetContainer, int targetSlot, int amount, ItemMoveModifier itemMoveModifier) { if (!PluginIsLoaded) { return; } if (item == null || playerLoot.baseEntity == null || !playerLoot.baseEntity.IsRealPlayer()) { return; } var sourceContainer = item.GetRootContainer(); var curSlot = item.position; var basePlayer = playerLoot.baseEntity; NextTick(() => { if (item == null) { return; } var destContainer = item.GetRootContainer(); if (destContainer == null) { return; } if (itemMoveModifier == ItemMoveModifier.BackpackOpen) { // Backpack handler var backpack = basePlayer.inventory.containerWear.itemList.FirstOrDefault(x => x.IsBackpack()).contents; if (backpack != null) { InventoryOverlay.UpdateAt(basePlayer, backpack, curSlot, true); if (backpack.uid == targetContainer) { destContainer = backpack; } } } if (sourceContainer != null && (sourceContainer.uid == destContainer.uid || !sourceContainer.IsPlayerContainer(basePlayer))) { if (!sourceContainer.IsPlayerContainer(basePlayer) && (destContainer?.IsPlayerContainer(basePlayer) ?? false)) { Interface.Call("QC_ItemMovedToInventory", basePlayer, sourceContainer, destContainer, item); } InventoryOverlay.UpdateAt(basePlayer, sourceContainer, curSlot, true); // OnItemRemovedFromContainer already handles this } InventoryOverlay.UpdateAt(basePlayer, destContainer, item.position, true); }); } #endregion #region Perks // OnDamageTaken && OnDamageDealt void OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info) { if (!PluginIsLoaded) { return; } if (entity == null || info == null) { return; } // Attack Modifier if (info.InitiatorPlayer != null) { var attackItem = info.Weapon?.GetItem(); if (attackItem == null && info.WasThrown()) { attackItem = info.Weapon.GetCachedItem(); } if (attackItem != null) { if (attackItem.IsQualityItem()) { var qi = attackItem.QualityIfExists(); if (qi != null) { foreach (var itemPerk in qi.Perks.Where(x => x.Perk.OnDamageDealt != null).OrderByDescending(x => x.Perk.Priority)) { itemPerk.Perk.OnDamageDealt(itemPerk, qi, info); } info.damageTypes.ScaleAll(qi.DamageScale + qi.TemporaryScaleBonus); qi.TemporaryScaleBonus = 0f; } } // Stats if (CONFIG.SkillsMenu.ShowStatistics) { var stats = Statistics.Get(info.InitiatorPlayer.userID).Categories[CraftingCategoryType.Melee]; if (stats.DamageDealt == null) { stats.DamageDealt = 0f; } stats.DamageDealt += info.damageTypes.Total(); } } } if (entity is BasePlayer basePlayer) { OnPlayerTakeDamage(basePlayer, info); } // Statistics if (CONFIG.SkillsMenu.ShowStatistics && info.InitiatorPlayer != null && info.IsProjectile()) { var weapon = info.Weapon?.GetItem(); if (weapon != null) { var category = CraftingCategory.GetCraftingCategoryType(weapon); if (category == CraftingCategoryType.Bows && info.ProjectileDistance > 0f) { var stats = Statistics.Get(info.InitiatorPlayer.userID).Categories[CraftingCategoryType.Bows]; var distance = info.ProjectileDistance; if (stats.FarthestHit == null) { stats.FarthestHit = 0f; } if (distance > stats.FarthestHit) { stats.FarthestHit = distance; } } else if (category == CraftingCategoryType.Firearms && info.isHeadshot) { var stats = Statistics.Get(info.InitiatorPlayer.userID).Categories[CraftingCategoryType.Firearms]; if (stats.Headshots == null) { stats.Headshots = 0f; } stats.Headshots++; } } } } void OnPlayerTakeDamage(BasePlayer basePlayer, HitInfo info) { // Fake hook foreach (var item in basePlayer.WornItems()) { var qi = item.QualityIfExists(); if (qi == null || qi.Quality <= 0) { continue; } foreach (var dt in Const.AllDamageTypes) { var resistance = Math.Min(0.99f, ScaleDamageTypeValue(dt, qi.GetBonusResistance(dt, info.boneArea))); if (resistance == 0) { continue; } info.damageTypes.Scale(dt, 1f - resistance); } foreach (var itemPerk in qi.Perks) { itemPerk.Perk.OnDamageTaken?.Invoke(itemPerk, qi, info); } } // Statistics if (CONFIG.SkillsMenu.ShowStatistics && info != null && info.damageTypes.Total() > 0f) { var stats = Statistics.Get(basePlayer.userID).Categories[CraftingCategoryType.Clothing]; if (stats.DamageTaken == null) { stats.DamageTaken = 0f; } stats.DamageTaken += info.damageTypes.Total(); } } // OnWeaponThrown void OnWorldProjectileCreate(HitInfo info, Item item) { if (!PluginIsLoaded) { return; } if (item == null || info == null) { return; } var basePlayer = info.InitiatorPlayer; if (basePlayer == null) { return; } var qi = item.QualityIfExists(); if (qi == null || qi.Quality <= 0) { return; } foreach (var itemPerk in qi.Perks) { itemPerk.Perk.OnWeaponThrown?.Invoke(itemPerk, qi, basePlayer); } } // OnWeaponFired void OnWeaponFired(BaseProjectile projectile, BasePlayer basePlayer, ItemModProjectile mod, ProtoBuf.ProjectileShoot projectiles) { if (!PluginIsLoaded) { return; } if (projectile == null || basePlayer == null) { return; } var item = projectile.GetItem(); if (item == null) { return; } var qi = item.QualityIfExists(); if (qi == null || qi.Quality <= 0) { return; } foreach (var itemPerk in qi.Perks) { itemPerk.Perk.OnWeaponFired?.Invoke(itemPerk, qi, basePlayer, projectile); } } // OnResourceHit void OnDispenserGather(ResourceDispenser dispenser, BasePlayer basePlayer, Item resource) { if (!PluginIsLoaded) { return; } if (dispenser == null || basePlayer == null || resource == null) { return; } var totalBonusMultiplier = 0f; foreach (var item in basePlayer.EquippedItems()) { var qi = item.QualityIfExists(); if (qi == null || qi.Quality <= 0) { continue; } totalBonusMultiplier += qi.GatherScaleBonus; foreach (var itemPerk in qi.Perks) { itemPerk.Perk.OnResourceHit?.Invoke(itemPerk, qi, basePlayer, dispenser, resource); } } if (CONFIG.SkillsMenu.ShowStatistics) { var stats = Statistics.Get(basePlayer.userID).Categories[CraftingCategoryType.Tools]; if (stats.ResourcesGained == null) { stats.ResourcesGained = 0; } stats.ResourcesGained += resource.amount; } } // OnItemSwapped OnItemUnswapped void OnActiveItemChanged(BasePlayer basePlayer, Item oldItem, Item newItem) { if (basePlayer == null) { return; } if (oldItem != null) { var qi = oldItem.QualityIfExists(); if (qi?.Quality > 0) { foreach (var itemPerk in qi.Perks) { itemPerk.Perk.OnItemUnswapped?.Invoke(itemPerk, qi, basePlayer); } } } if (newItem != null) { var qi = newItem.QualityIfExists(); if (qi?.Quality > 0) { foreach (var itemPerk in qi.Perks) { itemPerk.Perk.OnItemSwapped?.Invoke(itemPerk, qi, basePlayer); } } } } // OnEntityKilled void OnEntityDeath(BaseCombatEntity entity, HitInfo info) { if (entity == null || info == null) { return; } var attacker = info.InitiatorPlayer; if (attacker == null) { return; } var items = attacker.EquippedItems().ToList(); var weaponItem = info.Weapon?.GetCachedItem(); if (weaponItem != null && !items.Any(x => x.uid != weaponItem.uid)) { items.Add(weaponItem); } foreach(var item in items) { var qi = item?.QualityIfExists(); if (qi?.Quality > 0) { foreach (var itemPerk in qi.Perks) { itemPerk.Perk.OnEntityKilled?.Invoke(itemPerk, qi, attacker, entity, info); } } } items = null; } // OnClothingEquipped void OnClothingEquipped(BasePlayer basePlayer, Item item) { if (basePlayer == null || item == null) { return; } var qi = item.QualityIfExists(); if (qi?.Quality <= 0) { return; } foreach(var itemPerk in qi.Perks) { itemPerk.Perk.OnClothingEquipped?.Invoke(itemPerk, qi, basePlayer); } } // OnClothingUnequipped void OnClothingUnequipped(BasePlayer basePlayer, Item item) { if (basePlayer == null || item == null) { return; } var qi = item.QualityIfExists(); if (qi?.Quality <= 0) { return; } foreach (var itemPerk in qi.Perks) { itemPerk.Perk.OnClothingUnequipped?.Invoke(itemPerk, qi, basePlayer); } } /* Additional Perk Hooks Not Listed Here * - OnItemCreated * - OnPlayerTick */ #endregion } } namespace Oxide.Plugins { public partial class QualityCrafting : CovalencePlugin { public void AddPlayerTickBehavior(BasePlayer basePlayer) { if (basePlayer == null || !basePlayer.IsConnected) { return; } if (!_behaviours.ContainsKey(basePlayer.userID)) { var obj = basePlayer.gameObject.AddComponent(); _behaviours.Add(basePlayer.userID, obj); } } private void RemovePlayerTickBehavior(ulong userId) { var obj = _behaviours.GetValueOrDefault(userId); if (obj == null) { return; } UnityEngine.Object.Destroy(obj); _behaviours.Remove(userId); } public void RemoveAndDestroyAllPlayerTickBehaviors() { foreach (var b in _behaviours.Values) { if (b == null) { continue; } RemovePlayerTickBehavior(b.UserId); } _behaviours.Clear(); } private Dictionary _behaviours = new Dictionary(); public class PlayerTickBehavior : MonoBehaviour { private BasePlayer basePlayer; public ulong UserId => basePlayer.userID; private void Awake() { basePlayer = GetComponent(); StartWorking(); } private void OnDestroy() { StopWorking(); } private void StartWorking() { InvokeRepeating(nameof(DoPlayerTick), 10, 10); } private void StopWorking() { CancelInvoke(nameof(DoPlayerTick)); } private void DoPlayerTick() { if (basePlayer == null) { return; } foreach (var item in basePlayer.EquippedItems()) { if (item == null) { continue; } var qi = item.QualityIfExists(); if (qi == null || !qi.AnyPerks) { continue; } foreach (var itemPerk in qi.Perks?.Where(x => x.Perk.OnPlayerTick != null).OrderByDescending(x => x.Perk.Priority)) { itemPerk?.Perk?.OnPlayerTick(itemPerk, qi, basePlayer); } } } } } } namespace Oxide.Plugins { public partial class QualityCrafting { // //[Command("qcadmin")] // //private void TestAdmin(IPlayer player, string command, string[] args) // //{ // // if (!player.IsAdmin) { return; } // // var basePlayer = player.BasePlayer(); // // var ui = SCOPE.Player(basePlayer); // // var id = "admincontrol"; // // var w = 100; // // var h = 20; // // var offset = 100; // // var u = new UI // // { // // ID = id, // // Parent = "Overlay", // // Origin = Origin.MiddleCenter, // // Color = Color(a: 0.5f), // // Height = 300, // // Width = 300, // // NeedsCursor = true, // // Children = // // { // // new UI // // { // // Text = "Up", // // TextAlign = UnityEngine.TextAnchor.MiddleCenter, // // Width = w, // // Height = h, // // Origin = Origin.UpperCenter, // // Button = ClickAction((args) => // // { // // config.Tracking.Y += 10; // // Tracking.Show(basePlayer); // // }) // // }, // // new UI // // { // // Text = "Down", // // TextAlign = UnityEngine.TextAnchor.MiddleCenter, // // Width = w, // // Height = h, // // Origin = Origin.LowerCenter, // // Button = ClickAction((args) => // // { // // config.Tracking.Y -= 10; // // Tracking.Show(basePlayer); // // }) // // }, // // new UI // // { // // Text = "Left", // // TextAlign = UnityEngine.TextAnchor.MiddleCenter, // // Width = w, // // Height = h, // // Origin = Origin.MiddleLeft, // // Button = ClickAction((args) => // // { // // config.Tracking.X -= 10; // // Tracking.Show(basePlayer); // // }) // // }, // // new UI // // { // // Text = "Right", // // TextAlign = UnityEngine.TextAnchor.MiddleCenter, // // Width = w, // // Height = h, // // Origin = Origin.MiddleRight, // // Button = ClickAction((args) => // // { // // config.Tracking.X += 10; // // Tracking.Show(basePlayer); // // }) // // }, // // new UI // // { // // Origin = Origin.MiddleCenter, // // Width = w, // // Height = h*3, // // Layout = new UiLayout // // { // // Rows = 3 // // }, // // Children = // // { // // new UI // // { // // Text = "Save", // // TextAlign = UnityEngine.TextAnchor.MiddleCenter, // // Button = ClickAction((args) => // // { // // SaveConfig(); // // PrintWarning("Config values have been updated"); // // }) // // }, // // new UI // // { // // Text = "Exit", // // TextAlign = UnityEngine.TextAnchor.MiddleCenter, // // Button = ClickAction((args) => // // { // // var ui = SCOPE.Player(basePlayer); // // ui.Destroy(id); // // }) // // } // // } // // } // // } // // }; // // ui.Show(u); // //} // private const bool TESTS_ENABLED = true; // [Command("comp")] // private void TestComp(IPlayer player, string command, string[] args) // { // if (!TESTS_ENABLED || !player.IsAdmin) { return; } // var basePlayer = player.BasePlayer(); // var expression = args[0]; // var itemLevel = args[1]; // var quality = args[2]; // expression = expression.Replace("level", itemLevel.ToString()).Replace("quality", quality.ToString()); // var table = new DataTable(); // var val = table.Compute(expression, ""); // player.Message($"{expression}={val}"); // } // [Command("nt")] // private void TestNt(IPlayer player, string command, string[] args) // { // if (!TESTS_ENABLED || !player.IsAdmin) { return; } // var basePlayer = player.BasePlayer(); // var entity = GetObjectRaycast(basePlayer, 4f); // var item = basePlayer.GetActiveItem(); // var qi = item.Quality(); // Notifications.Show(basePlayer, qi, true); // } // [Command("give")] // private void TestGive(IPlayer player, string command, string[] args) // { // if (!TESTS_ENABLED || !player.IsAdmin) { return; } // var basePlayer = player.BasePlayer(); // var entity = GetObjectRaycast(basePlayer, 4f); // var item = basePlayer.GetActiveItem(); // entity.inventory.containerWear.GiveItem(item); // //entity.inventory.containerBelt.GiveItem(item); // player.Message($"Give {item.info.name} to {entity.displayName}"); // } // [Command("perks")] // private void TestPerks(IPlayer player, string command, string[] args) // { // if (!TESTS_ENABLED || !player.IsAdmin) { return; } // var basePlayer = player.BasePlayer(); // var ui = SCOPE.Player(basePlayer); // var u = new UI // { // ID = "qc.perkslist", // Parent = "Overlay", // Color = Color("0 0 0 0.9"), // Origin = Origin.Full, // Button = ClickAction((args) => // { // SCOPE.Player(basePlayer).Destroy("qc.perkslist"); // }), // NeedsCursor = true, // Children = // { // new UI // { // Origin = D4(0.1f, 0.1f, 0.9f, 0.9f), // Layout = new UiLayout // { // Cols = 4, // Rows = 6, // RowHeight = 80, // ColWidth = 200, // Gaps = D2(8, 8) // }, // Children = UI.ForEach(Perks.ALL.Values.Where(x => !x.IsNegative), (perk) => // { // return new UI // { // Trim = D4(4), // Color = COLOR.Rust.DarkGray, // Children = // { // new UI // { // Color = Color(perk.Color), // Height = 28, // GrowY = Growth.Negative, // Origin = Origin.RowUpper, // Children = // { // new UI // { // Position = D4(right: 6), // Height = 20, // Width = 20, // Origin = Origin.MiddleLeft, // Image = perk.IconID, // ImageType = ImageType.Simple, // ImageColor = Color(perk.Color).Saturate(0.5f) // }, // new UI // { // Trim = D4(left: 10 + 16 + 6), // Text = $"{perk.NameLocalized(basePlayer)}", // TextAlign = TextAnchor.MiddleLeft, // TextColor = Color(perk.Color).Saturate(0.5f), // TextSize = 14 // } // } // }, // new UI // { // Trim = D4(top: 24, left: 6, right: 6), // Text = perk.DescriptionLocalized(basePlayer), // TextColor = COLOR.Rust.LightBrown, // TextSize = 12, // TextAlign = TextAnchor.MiddleLeft // } // } // }; // }) // } // } // }; // ui.Show(u); // } // //[Command("savecsv")] // //private void TestSaveCsv(IPlayer player, string command, string[] args) // //{ // // if (!TESTS_ENABLED || !player.IsAdmin) { return; } // // var data = "1.0,2.0,3.0,4.0,5.0,6.0,7.0"; // // SaveCsv("letters", data); // // player.Message("Saved"); // //} // //[Command("loadcsv")] // //private void TestLoadCsv(IPlayer player, string command, string[] args) // //{ // // if (!TESTS_ENABLED || !player.IsAdmin) { return; } // // var filename = args[0]; // // CSV = LoadCsv(filename); // // player.Message($"{CSV}"); // //} // [Command("heal")] // private void TestHeal(IPlayer player, string command, string[] args) // { // if (!TESTS_ENABLED || !player.IsAdmin) { return; } // var basePlayer = player.BasePlayer(); // var entity = GetObjectRaycast(basePlayer, 4f); // entity._health = 100f; // player.Message($"Healed {entity.displayName}"); // } // [Command("mhp")] // private void TestMhp(IPlayer player, string command, string[] args) // { // if (!TESTS_ENABLED || !player.IsAdmin) { return; } // var basePlayer = player.BasePlayer(); // var entity = GetObjectRaycast(basePlayer, 4f); // entity._maxHealth = 1000f; // entity._health = 1000f; // player.Message($"MHP {entity._maxHealth}"); // } // //[Command("sleep")] // //private void TestSleep(IPlayer player, string command, string[] args) // //{ // // if (!TESTS_ENABLED || !player.IsAdmin) { return; } // // var basePlayer = player.BasePlayer(); // // var entity = GetObjectRaycast(basePlayer, 4f); // // entity.StartSleeping(); // //} // [Command("o")] // private void TestOInfo(IPlayer player, string command, string[] args) // { // var basePlayer = player.BasePlayer(); // if (!InventoryOverlay.PlayerOverlays.ContainsKey(basePlayer.userID)) { return; } // var info = InventoryOverlay.PlayerOverlays.GetValueOrDefault(basePlayer.userID); // player.Message($"INFO: hotbar={info.HotbarIsShown} main={info.MainIsShown} wear={info.WearIsShown} storage={info.StorageIsShown}"); // } // [Command("ranks")] // private void TestRanks(IPlayer player, string command, string[] args) // { // if (!TESTS_ENABLED) { return; } // player.Message(Leaderboards.CategoryRankings[CraftingCategoryType.Melee].Select(x => FindPlayer(x.UserID)?.Name).ToSentence()); // } // [Command("me")] // private void TestMe(IPlayer player, string command, string[] args) // { // if (!TESTS_ENABLED) { return; } // var basePlayer = player.BasePlayer(); // var item = basePlayer.GetActiveItem(); // var heldEntity = item.GetHeldEntity() as BaseProjectile; // } // [Command("ecount")] // private void TestECount(IPlayer player, string command, string[] args) // { // if (!TESTS_ENABLED) { return; } // var str = "\n######################\n"; // str += $"ELEMENTS: {SCOPE.TotalActiveElements()}|\n"; // str += $"INSTANCES: {SCOPE.InstanceCount()}|\n"; // str += $"PLAYERS ONLINE: {BasePlayer.activePlayerList.Count()}|\n"; // str += $"CACHED QUALITY ITEMS: {CachedQualityItems.Count}|\n"; // str += $"SAVED SKILLS: {PlayerCraftingSkills.Count}|\n"; // str += $"PLAYER OVERLAYS: {InventoryOverlay.PlayerOverlays.Count}|\n"; // str += "#####################\n"; // Puts(str); // } // //[Command("json")] // //private void TestJson(IPlayer player, string command, string[] args) // //{ // // timer.In(1f, () => // // { // // var container = new CuiElementContainer(); // // var c = new CuiElement // // { // // Name = "json", // // Parent = "Overlay", // // Components = // // { // // new CuiInputFieldComponent // // { // // NeedsKeyboard = true // // }, // // new CuiRectTransformComponent // // { // // AnchorMin = "0 0", // // AnchorMax = "0.1 0.1" // // } // // } // // }; // // container.Add(c); // // Puts(container.ToJson()); // // CuiHelper.AddUi(player.Object as BasePlayer, container); // // }); // //} // //[Command("jsonc")] // //private void TestJsonC(IPlayer player, string command, string[] args) // //{ // // CuiHelper.DestroyUi(player.Object as BasePlayer, "json"); // //} // [Command("cancraft")] // private void TestCanCraft(IPlayer player, string command, string[] args) // { // var basePlayer = player.BasePlayer(); // var item = basePlayer.GetActiveItem(); // if (item == null) { return; } // var canCraft = item.IsQualityCraftable(); // basePlayer.Chat((canCraft ? "This item can be quality craftable." : "You cannot craft quality versions of this item.")); // } // [Command("itemperks")] // private void TestItemPerks(IPlayer player, string command, string[] args) // { // var basePlayer = player.BasePlayer(); // var item = basePlayer.GetActiveItem(); // if (item == null) { return; } // var qi = item.QualityIfExists(); // if (qi == null) { return; } // basePlayer.Chat($"Item perks are: {(qi.Perks.Select(x => x.PerkID).ToSentence())}"); // } // [Command("gray")] // private void TestGray(IPlayer player, string command, string[] args) // { // var basePlayer = player.BasePlayer(); // var ui = new UI // { // ID = "gray", // Parent = "Overlay", // Origin = Origin.MiddleCenter, // Position = P4(left: 260f, up: 60), // Height = 400, // Width = 100, // //Image = "assets/content/ui/ui.background.tiletex.psd", // Image = "assets/content/ui/ui.background.tiletex.psd", // ImageType = ImageType.Sprite, // ImageColor = Color("0.969 0.922 0.882 0.2"), // ImageMaterial = "assets/content/ui/uibackgroundblur-ingamemenu.mat" // }; // SCOPE.Player(basePlayer).Show(ui, true); // // CuiHelper.DestroyUi(basePlayer, "gray"); // // CuiHelper.AddUi(basePlayer, @"[ // // { // // ""name"": ""gray"", // // ""destroyui"": ""gray"", // // ""update"": false, // // ""parent"": ""Overlay"", // // ""enabled"": true, // // ""components"": [ // // { // // ""type"": ""RectTransform"", // // ""anchormin"": ""0.5 0.19"", // // ""anchormax"": ""0.5 0.19"", // // ""offsetmin"": ""-50 -50"", // // ""offsetmax"": ""50 50"" // // }, // // { // // ""type"": ""UnityEngine.UI.Image"", // // ""material"": ""assets/icons/iconmaterial.mat"", // // ""sprite"": ""assets/content/ui/ui.background.tiletex.psd"", // // ""color"": ""0.969 0.922 0.882 0.135"" // // } // // ] // // } // //]"); // } //[Command("i")] //private void TestI(IPlayer player, string command, string[] args) //{ // //if (!TESTS_ENABLED) { return; } // var basePlayer = player.BasePlayer(); // var item = basePlayer.GetActiveItem(); // if (item == null) { return; } // List info = new List(); // info.Add($"UID: {item.uid}"); // info.Add($"Text: {item.text}"); // info.Add($"ShortName: {item.info.shortname}"); // info.Add($"ItemID: {item.info.itemid}"); // info.Add($"HasSkins: {item.info.HasSkins}"); // info.Add($"Skins: {item.info.skins.Select(x => x.name).ToSentence()}"); // info.Add($"Skins2: {item.info.skins2.Select(x => x.Name).ToSentence()}"); // info.Add($"Skin: {item.skin}"); // info.Add($"SteamDLC: {item.info.steamDlc}"); // info.Add($"SteamItem: {item.info.steamItem}"); // info.Add($"SteamDLC: {item.info.flags}"); // info.Add($"IsRedirectOf: {item.info.isRedirectOf?.shortname}"); // info.Add($"Position: {item.position}"); // info.Add($"Type: {item.GetType()}"); // info.Add($"HeldEntityType: {item.GetHeldEntity()?.GetType()}"); // info.Add($"IsMod: {item.GetHeldEntity()?.GetType() == typeof(ProjectileWeaponMod)}"); // player.Message(string.Join('\n', info)); //} // [Command("jsons")] // private void TestRenames(IPlayer player, string command, string[] args) // { // if (!TESTS_ENABLED || !player.IsAdmin) { return; } // var basePlayer = player.BasePlayer(); // var json = @"[ // { // ""name"": ""gray"", // ""destroyui"": ""gray"", // ""update"": false, // ""parent"": ""Overlay"", // ""enabled"": true, // ""components"": [ // { // ""type"": ""RectTransform"", // ""anchormin"": ""0.5 0.5"", // ""anchormax"": ""0.5 0.5"", // ""offsetmin"": ""-310 -140"", // ""offsetmax"": ""-210 260"" // }, // { // ""type"": ""UnityEngine.UI.Image"", // ""material"": ""assets/content/ui/uibackgroundblur-ingamemenu.mat"", // ""sprite"": ""assets/content/ui/ui.background.tiletex.psd"", // ""color"": ""0.969 0.922 0.882 0.2"" // } // ] // } //]"; // CuiHelper.DestroyUi(basePlayer, "gray"); // CuiHelper.AddUi(basePlayer, json); // } // [Command("rename")] // private void TestRename(IPlayer player, string command, string[] args) // { // if (!TESTS_ENABLED || !player.IsAdmin) { return; } // var basePlayer = player.BasePlayer(); // var item = basePlayer.GetActiveItem(); // var qi = item.QualityIfExists(); // qi?.Rename(args[0], args[1]); // basePlayer.ChatMessage($"Renamed to {qi.DisplayName} {qi.Description}"); // } // [Command("mm")] // private void TestMM(IPlayer player, string command, string[] args) // { // if (!TESTS_ENABLED || !player.IsAdmin) { return; } // var basePlayer = player.BasePlayer(); // var item = basePlayer.GetActiveItem(); // basePlayer.inventory.containerWear.GiveItem(item); // } // private T GetObjectRaycast(BasePlayer basePlayer, float distance) where T : BaseEntity // { // Ray ray = new Ray(basePlayer.eyes.position, basePlayer.eyes.HeadForward()); // RaycastHit hit; // var entity = !UnityEngine.Physics.Raycast(ray, out hit, distance) ? null : hit.GetEntity(); // if (entity == null || !(entity is T)) // { // return null; // } // return (T)entity; // } } } namespace Oxide.Plugins { public partial class QualityCrafting { public static void BackupData() { Leveling.SavePlayerCraftingSkills("backups"); Statistics.SaveStatistics("backups"); INSTANCE.PrintWarning("PlayerData backup created"); } public static void BackupAndClearData() { BackupData(); INSTANCE.PlayerCraftingSkills.Clear(); Tracking.PlayerTracking.Clear(); Statistics.Clear(); foreach (var player in BasePlayer.activePlayerList) { INSTANCE.InitQualityCraftingPlayer(player); } INSTANCE.SaveAllData(); INSTANCE.PrintWarning("Player data has been cleared"); } public static void RestoreDataFromBackup() { var data = Interface.Oxide.DataFileSystem.ReadObject>($"{INSTANCE.Name}/backups/PlayerCraftingSkills") ?? new Dictionary(); INSTANCE.PlayerCraftingSkills = data; Statistics.LoadStatistics("backups"); foreach (var player in BasePlayer.activePlayerList) { INSTANCE.InitQualityCraftingPlayer(player); } INSTANCE.SaveAllData(); INSTANCE.PrintWarning("Player data has been restored from backup file"); } } } namespace Oxide.Plugins { public partial class QualityCrafting { } } namespace Oxide.Plugins { public partial class QualityCrafting : CovalencePlugin { [Command("qc.clothinglayer")] private void CmdClothingLayer(IPlayer player, string command, string[] args) { // [0] = layer BasePlayer basePlayer = player.BasePlayer(); if (basePlayer == null) { return; } var layer = args[0]; var oldLayer = ClothingStatsOverlay.PlayerSelectedLayer.GetValueOrDefault(basePlayer.userID, "body"); if (layer == oldLayer) { return; } ClothingStatsOverlay.SetSelectedLayer(basePlayer.userID, layer); ClothingStatsOverlay.UpdateLayerButton(basePlayer, oldLayer); ClothingStatsOverlay.UpdateLayerButton(basePlayer, layer); ClothingStatsOverlay.Update(basePlayer); } public static class ClothingStatsOverlay { private static string ParentID => InventoryWear.id; private const string ID = "overlay.clothingstats"; private const int SpriteSize = 20; private const int Padding = 8; //private const string BackgroundColor = "0.969 0.922 0.882 1"; private const string BackgroundColor = "0.2735 0.261 0.241 1"; private const string BackgroundMaterial = "assets/content/ui/uibackgroundblur-ingamemenu.mat"; private const string BackgroundSprite = "assets/content/ui/ui.background.tiletex.psd"; private const int ButtonSize = 16; private static readonly UiColor TextColorHide = COLOR.Rust.White.Set(a: 0.3f); private static readonly UiColor TextColorShown = COLOR.Rust.White; private static readonly UiColor TextColorBonus = COLOR.Rust.LightLime; private static readonly UiColor TextColorNegative = COLOR.Rust.LightOrange; public static readonly Dictionary PlayerSelectedLayer = new Dictionary(); private static string ValueID(Rust.DamageType dt) => $"{ID}.{dt}"; private static string ButtonID(string layer) => $"{ID}.{layer}"; private static Dictionary GetStatsForSelectedLayer(BasePlayer basePlayer, string selected, ClothingResistances resistances) { return selected == "head" ? resistances.Head : selected == "legs" ? resistances.Legs : resistances.Body; } private static UiColor GetColor(float value, bool isBonus) { return (value < 0.01f && value > -0.01f) ? TextColorHide : value <= -0.01f ? TextColorNegative : isBonus ? TextColorBonus : TextColorShown; } private static void UpdateTextComponent(UiEnginePlayer ui, Dictionary stats, Rust.DamageType dt, bool hasBonus) { var id = ValueID(dt); // Text var textElement = ui.Get(id); if (textElement.IsNull) { return; } var text = textElement.GetComponent(); if (text.IsNull) { return; } var newValue = ScaleDamageTypeValue(dt, stats.GetValueOrDefault(dt)); var color = GetColor(newValue, hasBonus); text.Text = newValue.Percent(); text.Color = color; text.FadeIn = 0f; textElement.Components.RemoveAll(x => x is UiTextComponent); textElement.Components.Add(text); ui.Update(textElement); // Sprite var spriteElement = ui.Get(id + "sprite"); if (spriteElement.IsNull) { return; } var sprite = spriteElement.GetComponent(); if (sprite.IsNull) { return; } sprite.Color = color; sprite.FadeIn = 0f; spriteElement.Components.RemoveAll(x => x is UiImageComponent); spriteElement.Components.Add(sprite); ui.Update(spriteElement); } public static void UpdateLayerButton(BasePlayer basePlayer, string layer) { var ui = SCOPE.Player(basePlayer); var isSelected = PlayerSelectedLayer.GetValueOrDefault(basePlayer.userID) == layer; var color = isSelected ? TextColorShown : TextColorHide; var id = ButtonID(layer); // Button var buttonElement = ui.Get(id); if (buttonElement.IsNull) { return; } var button = buttonElement.GetComponent(); if (button.IsNull) { return; } button.Color = color; buttonElement.Components.RemoveAll(x => x is UiImageComponent); buttonElement.Components.Add(button); ui.Update(buttonElement); // Text var textElement = ui.Get(id + "text"); if (textElement.IsNull) { return; } var text = textElement.GetComponent(); if (text.IsNull) { return; } text.Color = isSelected ? color : COLOR.Transparent; textElement.Components.RemoveAll(x => x is UiTextComponent); textElement.Components.Add(text); ui.Update(textElement); } public static void SetSelectedLayer(ulong userId, string layer) { if (layer == null) { PlayerSelectedLayer.Remove(userId); return; } PlayerSelectedLayer[userId] = layer; } public static void Show(BasePlayer basePlayer) { if (!CONFIG.InventoryOverlay.ShowClothingModifiers) { return; } var ui = SCOPE.Player(basePlayer); if (basePlayer.inventory.containerWear.itemList.Count == 0) { SetSelectedLayer(basePlayer.userID, null); ui.Destroy(ID); return; } var clothingDamageTypes = QualityItem.GetClothingProtectionStats(); var resistances = basePlayer.Resistances(); var layer = PlayerSelectedLayer.GetValueOrDefault(basePlayer.userID, "body"); var stats = GetStatsForSelectedLayer(basePlayer, layer, resistances); var u = new UI { Parent = ParentID, ID = ID, FadeIn = 0.1f, Children = { new UI { Height = 100, Width = 28, GrowX = Growth.Negative, GrowY = Growth.Neutral, Origin = Origin.UpperRight, Position = P4(up: 237, left: 85), Layout = new UiLayout { Rows = 3, Cols = 1 }, Children = UI.For(3, (i) => { var layer = i == 0 ? "head" : i == 1 ? "body" : "legs"; var id = ButtonID(layer); var selected = layer == PlayerSelectedLayer.GetValueOrDefault(basePlayer.userID, "body"); var color = selected ? TextColorShown : TextColorHide; return new UI { ID = id, Button = ClickCommand($"qc.clothinglayer {layer}"), Origin = Origin.MiddleCenter, ButtonImage = SPRITE.Clothing, Height = ButtonSize, Width = ButtonSize, ButtonImageColor = color, Children = { new UI { ID = id + "text", Text = basePlayer.Lang($"layer {layer}").ToUpper(), InheritFade = false, TextColor = selected ? color : COLOR.Transparent, Origin = Origin.ColLeft, TextSize = 8, Width = 24, TextAlign = TextAnchor.MiddleLeft, GrowX = Growth.Negative } } }; }) }, new UI { Color = Color(BackgroundColor), Material = BackgroundMaterial, Height = 434, Width = 80, GrowX = Growth.Negative, GrowY = Growth.Positive, Origin = Origin.UpperRight, Position = P4(up: 20), Padding = D4(Padding), Layout = new UiLayout { Rows = clothingDamageTypes.Count, Cols = 2 }, Children = UI.ForEach(clothingDamageTypes, (dt) => { var value = ScaleDamageTypeValue(dt.DamageType, stats.GetValueOrDefault(dt.DamageType)); var hasBonus = resistances.TypesWithBonuses.Contains(dt.DamageType); var color = GetColor(value, hasBonus); var id = ValueID(dt.DamageType); return new UI[] { new UI { ID = id, Origin = Origin.ColLeft, Width = 30, Text = value.Percent(), TextColor = color, TextAlign = TextAnchor.MiddleRight }, new UI { ID = id + "sprite", Origin = Origin.MiddleRight, Image = dt.Sprite, ImageType = ImageType.Sprite, ImageColor = color, Height = SpriteSize, Width = SpriteSize } }; }) } } }; ui.Show(u); } public static void Update(BasePlayer basePlayer) { if (!CONFIG.InventoryOverlay.ShowClothingModifiers) { return; } var ui = SCOPE.Player(basePlayer); if (basePlayer.inventory.containerWear.itemList.Count == 0) { SetSelectedLayer(basePlayer.userID, null); ui.Destroy(ID); return; } else if (!ui.ActiveElementsIds.Contains(ID)) { Show(basePlayer); } var clothingDamageTypes = QualityItem.GetClothingProtectionStats(); var resistances = basePlayer.Resistances(); var layer = PlayerSelectedLayer.GetValueOrDefault(basePlayer.userID, "body"); var stats = GetStatsForSelectedLayer(basePlayer, layer, resistances); foreach(var dt in clothingDamageTypes) { var hasBonus = resistances.TypesWithBonuses.Contains(dt.DamageType); UpdateTextComponent(ui, stats, dt.DamageType, hasBonus); } } } } } namespace Oxide.Plugins { public partial class QualityCrafting { private static bool HasBegunImport = false; #region Reusable Arguments public static class QCArgument { public static readonly CommandMethodArgument Player = new CommandMethodArgument("player") { Validate = (player, method, arg, args, given) => { var target = FindPlayer(given); return target != null ? CommandMethodValidation.SUCCESS : new CommandMethodValidation($"No player with name or user id matching '{given}'"); } }; public static readonly CommandMethodArgument OpSet = new CommandMethodArgument("op") { AllowedValues = { "set" } }; public static readonly CommandMethodArgument OpBackupClearRestoreImport = new CommandMethodArgument("op") { AllowedValues = { "backup", "clear", "restore", "import" } }; public static readonly CommandMethodArgument OpAddRemoveClear = new CommandMethodArgument("op") { AllowedValues = { "add", "remove", "clear" } }; public static readonly CommandMethodArgument OpSetAddRemove = new CommandMethodArgument("op") { AllowedValues = { "add", "set", "remove" } }; public static readonly CommandMethodArgument Type = new CommandMethodArgument("type") { AllowedValues = { "item", "category" } }; public static readonly CommandMethodArgument ID = new CommandMethodArgument("id") { Validate = (player, method, arg, args, given) => { var type = method.GetArgValue("type", args); if (type == "item") { var itemDef = ItemManager.CreateByName(given); if (itemDef == null) { return new CommandMethodValidation($"No item exists with name '{given}'"); } } else if (type == "category") { try { var category = Enum.Parse(given); } catch (Exception) { return new CommandMethodValidation($"No category exists with name '{given}' valid options are {Enum.GetValues(typeof(CraftingCategoryType)).Cast().Select(x => x.ToString()).ToSentence()}. They are case sensitive"); } } return CommandMethodValidation.SUCCESS; } }; public static readonly CommandMethodArgument Amount = new CommandMethodArgument("amount") { Validate = (player, method, arg, args, given) => { int amount = 0; if (!int.TryParse(given, out amount) || amount < 0 || amount > 100) { return new CommandMethodValidation(CommandMethodReason.ValueOutOfRange.FormatAs(arg.Name, 0, 100)); } return CommandMethodValidation.SUCCESS; } }; public static readonly CommandMethodArgument Quality = new CommandMethodArgument("quality") { Validate = (player, method, arg, args, given) => { int amount = 0; if (!int.TryParse(given, out amount) || amount < 0 || amount > 5) { return new CommandMethodValidation(CommandMethodReason.ValueOutOfRange.FormatAs(arg.Name, 0, 5)); } return CommandMethodValidation.SUCCESS; } }; public static readonly CommandMethodArgument Perk = new CommandMethodArgument("perk") { Validate = (player, method, arg, args, given) => { // todo perk exists return CommandMethodValidation.SUCCESS; } }; public static readonly CommandMethodArgument PerkLevel = new CommandMethodArgument("level") { Validate = (player, method, arg, args, given) => { int amount = 0; if (!int.TryParse(given, out amount) || amount < 0 || amount > 100) { return new CommandMethodValidation(CommandMethodReason.ValueOutOfRange.FormatAs(arg.Name, 0, 100)); } return CommandMethodValidation.SUCCESS; } }; } #endregion #region Command Classes public class CommandMethod { public string Path { get; private set; } = ""; public string Description { get; private set; } = ""; public string Permission { get; set; } = PermissionAdmin; public List Examples { get; set; } = new List(); public List Args { get; set; } = new List(); public Func Validate { get; set; } = ((method, player, args) => { return CommandMethodValidation.SUCCESS; }); public Action Action { get; set; } = ((method, player, command, args) => { player.Message("Success!"); }); public CommandMethod(string path, string description) { Path = path; Description = description; } public CommandMethodValidation IsValid(CommandMethod method, IPlayer player, ref string[] param) { int i = 0; foreach (var arg in Args) { if (!arg.Required) { continue; } if (param.Length <= i) { return new CommandMethodValidation(CommandMethodReason.NotEnoughArgs); } var p = param[i]; if (arg.AllowedValues.Any()) { if (!arg.AllowedValues.Contains(p)) { return new CommandMethodValidation(CommandMethodReason.ValueNotAllowed.FormatAs(arg.Name, arg.AllowedValues.ToSentence())); } } var r = arg.Validate(player, method, arg, param, p); if (!r.Result) { return r; } i++; } return Validate.Invoke(method, player, param); } public bool HasPermission(IPlayer player) => INSTANCE.permission.UserHasPermission(player.Id, Permission); public string GetArgValue(string name, string[] args) { var idx = Args.FindIndex(x => x.Name == name); if (idx >= args.Length) { return null; } return args[idx]; } public override string ToString() => $"qc {Path} {string.Join(" ", Args.Select(x => "<" + (x.AllowedValues.Any() ? string.Join("/", x.AllowedValues) : x.Name) + (x.Required ? string.Empty : "?") + ">"))}"; } public static class CommandMethodReason { public const string NotEnoughArgs = "Not enough arguments given"; public const string ValueNotAllowed = "Allowed values for {0} are '{1}'"; public const string ValueOutOfRange = "{0} must be between {1} and {2}"; public const string MustBeInGame = "Must be in game to use this command"; public const string ItemNotValid = "The item you are holding is not a valid item for this command"; public const string NotHoldingItem = "You must be holding an item to use this command"; } public class CommandMethodValidation { public bool Result { get; set; } = false; public string Reason { get; set; } public CommandMethodValidation(string reason) { Reason = reason; } public static readonly CommandMethodValidation SUCCESS = new CommandMethodValidation("success") { Result = true }; } public class CommandMethodArgument { public bool Required { get; private set; } = true; public string Name { get; private set; } public List AllowedValues { get; set; } = new List(); public Func Validate { get; set; } = (player, method, arg, args, given) => { return CommandMethodValidation.SUCCESS; }; public CommandMethodArgument(string name, bool required = true) { Name = name; Required = required; } } #endregion private static string CmdSignature() { var c1 = COLOR.Rust.LightGreen; var c2 = COLOR.Rust.LightMaroon; return ("[ " + "Q".Color(c1) + "uality".Color(c2) + " C".Color(c1) + "rafting".Color(c2) + " ]").Size(16); } private static string CmdSuccess() { return CmdSignature() + "\n\nThe command was successful!"; } [Command("qc")] private void CmdController(IPlayer player, string command, string[] args) { var allCommands = AllCommands.Where(x => x.HasPermission(player)); var noPerms = !allCommands.Any(); var path = args.GetValueOrDefault(0); var cm = path == null ? null : AllCommands.FirstOrDefault(x => x.Path.ToLower() == path.ToLower()); if ((noPerms) || (cm != null && !cm.HasPermission(player))) { player.Message("You do not have permission to use that command."); return; } if (args.Length == 0 || cm == null) { // handle var help = CmdSignature(); help += $"\n\n{string.Join("\n\n", allCommands.Select(x => $"{x.Description.Color(COLOR.Yellow)}\n{x}"))}".Size(12); player.Message(help); return; } var param = args.Skip(1).ToArray(); var result = cm.IsValid(cm, player, ref param); if (!result.Result) { var basePlayer = player.BasePlayer(); var playerName = basePlayer?.displayName ?? "mr01sam"; var yellow = COLOR.Yellow; // handle var str = CmdSignature(); str += $"\n\nThere was an error with the command you tried.".Color(COLOR.Rust.LightOrange).Size(12); str += $"\n\n{"Usage:".Color(yellow)}\n{cm}".Size(12); str += $"\n\n{"Description:".Color(yellow)}\n{cm.Description}".Size(12); str += $"\n\n{"Given:".Color(yellow)}\n/{command + " " + string.Join(" ", args)}".Size(12); str += $"\n\n{"Error:".Color(yellow)}\n{result.Reason}".Size(12); if (cm?.Examples.Any() ?? false) { var activeItem = basePlayer.GetActiveItem(); var activeItemName = activeItem == null ? "spear.wooden" : activeItem.info.shortname; var activeCategory = activeItem == null ? CraftingCategoryType.Melee : CraftingCategory.GetCraftingCategoryType(activeItem); str += $"\n\n{"Examples:".Color(yellow)}\n{string.Join("\n", cm.Examples.Select(x => string.Format(x, playerName, activeItemName, activeCategory.ToString())))}".Size(12); } player.Message(str); return; } cm.Action.Invoke(cm, player, command, param); } public static readonly List AllCommands = new List { new CommandMethod("level", "Modifies a player's skill level for a category or item.") { Args = { QCArgument.OpSetAddRemove, QCArgument.Player, QCArgument.Type, QCArgument.ID, QCArgument.Amount }, Examples = { "/qc level set \"{0}\" category \"{2}\" 99", "/qc level set \"{0}\" item \"{1}\" 99" }, Action = (method, player, command, args) => { var skills = player.Skills(); LevelXP prog = null; var type = method.GetArgValue("type", args); if (type == "category") { var category = Enum.Parse(method.GetArgValue("id", args)); prog = skills[category]; var value = int.Parse(method.GetArgValue("amount", args)); var op = method.GetArgValue("op", args); if (op == "add") {prog.SetLevel(prog.Level+value);} else if (op == "remove") {prog.SetLevel(prog.Level-value);} else {prog.SetLevel(value);} var basePlayer = player.Object as BasePlayer; if (basePlayer == null) { return; } var t = Tracking.Get(basePlayer.userID, category); Tracking.Refresh(basePlayer, t, true, 0); Leaderboards.RefreshPlayerRankings(category); if (SkillsMenu.HasMenuOpen(basePlayer.userID)) { SkillsMenu.ReloadCurrentContent(basePlayer);} PlaySFX(basePlayer, FX.LockCodeUpdated); } else { var itemShortName = method.GetArgValue("id", args); prog = skills[itemShortName]; var value = int.Parse(method.GetArgValue("amount", args)); var op = method.GetArgValue("op", args); if (op == "add") {prog.SetLevel(prog.Level+value);} else if (op == "remove") {prog.SetLevel(prog.Level-value);} else {prog.SetLevel(value);} var basePlayer = player.Object as BasePlayer; if (basePlayer == null) { return; } var t = Tracking.Get(basePlayer.userID, itemShortName); Tracking.Refresh(basePlayer, t, true, 0); if (SkillsMenu.HasMenuOpen(basePlayer.userID)) { SkillsMenu.ReloadCurrentContent(basePlayer);} PlaySFX(basePlayer, FX.LockCodeUpdated); } player.Chat(CmdSuccess()); } }, //new CommandMethod("xp", "Modifies a player's skill xp for a category or item.") //{ // Args = // { // QCArgument.OpSetAddRemove, // QCArgument.Player, // QCArgument.Type, // QCArgument.ID, // QCArgument.Amount // }, // Action = (method, player, command, args) => // { // } //}, new CommandMethod("quality", "Sets the quality with random perks for the item you are holding.") { Args = { QCArgument.OpSet, QCArgument.Quality }, Examples = { "/qc quality set \"wooden.spear\" 3" }, Validate = (method, player, args) => { var target = (BasePlayer)player.Object; if (target == null) { return new CommandMethodValidation(CommandMethodReason.MustBeInGame); } var activeItem = target.GetActiveItem(); if (activeItem == null) { return new CommandMethodValidation(CommandMethodReason.NotHoldingItem); } var qi = activeItem.Quality(); if (qi == null) { return new CommandMethodValidation(CommandMethodReason.ItemNotValid); } return CommandMethodValidation.SUCCESS; }, Action = (method, player, command, args) => { var target = (BasePlayer)player.Object; var item = target.GetActiveItem(); item.Quality().SetQuality(int.Parse(method.GetArgValue("quality", args))); InventoryOverlay.UpdateAt(target, target.inventory.containerBelt, item.position, true); player.Chat(CmdSuccess()); } }, new CommandMethod("perks", "Updates the perks for the item you are holding.") { Args = { QCArgument.OpSet, QCArgument.Perk, QCArgument.PerkLevel }, Validate = (method, player, args) => { var target = (BasePlayer)player.Object; if (target == null) { return new CommandMethodValidation(CommandMethodReason.MustBeInGame); } var activeItem = target.GetActiveItem(); if (activeItem == null) { return new CommandMethodValidation(CommandMethodReason.NotHoldingItem); } var qi = activeItem.Quality(); if (qi == null) { return new CommandMethodValidation(CommandMethodReason.ItemNotValid); } return CommandMethodValidation.SUCCESS; }, Action = (method, player, command, args) => { var target = (BasePlayer)player.Object; var item = target.GetActiveItem(); item.Quality().SetPerk(method.GetArgValue("perk", args), int.Parse(method.GetArgValue("level", args))); InventoryOverlay.UpdateAt(target, target.inventory.containerBelt, item.position, true); player.Chat(CmdSuccess()); } }, new CommandMethod("playerdata", "Performs an action on all player data. BE CAREFUL.") { Args = { QCArgument.OpBackupClearRestoreImport }, Action = (method, player, command, args) => { var op = method.GetArgValue("op", args); if (op == "clear") { BackupAndClearData(); player.Message("Player data has been cleared. A backup has been created first."); } else if (op == "backup") { BackupData(); player.Message("Player data has been saved to the backups folder."); } else if (op == "restore") { RestoreDataFromBackup(); player.Message("Player data has been restored from a backup."); } else if (op == "import") { if (!HasBegunImport) { player.Message("WARNING: You are about to import ALL player data from a legacy version of Quality Crafting. This will REPLACE all current data for players with existing data. It is recommended you make a backup of the player data first. You will not get this warning a second time.\n\nIf you wish to continue with the import, type this command again. "); HasBegunImport = true; return; } try { player.Message($"Importing legacy skills, this may take a moment..."); Leveling.ImportLegacyPlayerCraftingSkills((success, fails) => { player.Message($"Finished import with {success} successes and {fails} failures"); Leveling.SavePlayerCraftingSkills(); }); } catch (Exception e) { } } } } }; } } namespace Oxide.Plugins { public partial class QualityCrafting { public static Dictionary> WeightedPerksPerCategory = new Dictionary>(); public static Dictionary> PerksForCategory = new Dictionary>(); void OnQualtyItemCraftStart(ItemCraftTask task, BasePlayer basePlayer) { var skills = basePlayer.Skills(); task.blueprint = UnityEngine.Object.Instantiate(task.blueprint); var cat = CraftingCategory.GetCraftingCategoryType(task.blueprint.targetItem); var speed = skills.GetCraftingSpeed(cat); float mod = (float?) Interface.CallHook("QC_GetCraftingSpeedModifier", basePlayer) ?? 0; speed += mod; //var speed = 100; task.blueprint.time *= (1f / speed); } void OnQualityItemCrafted(BasePlayer basePlayer, Item item, ItemCraftTask task) { if (!item.IsQualityCraftable()) { return; } var qi = item.Quality(); var category = qi.Category; if (category == CraftingCategoryType.None || !category.IsEnabled()) { return; } var award = qi.XPAward(); var skills = basePlayer.Skills(); var numPerks = skills.GetPerkCount(qi.ShortName); qi.SetQuality(numPerks); NextTick(() => { if (qi?.Item == null) { return; } InventoryOverlay.UpdateAt(basePlayer, qi.Item.GetRootContainer(), qi.Item.position, false); }); var roll = Roll(); var needed = skills.GetDuplicateChance(qi.Category) * 100; var duplicated = qi.Quality > 0 && roll.MeetsOrBeats(needed); if (duplicated) { var qi2 = qi.Duplicate(); basePlayer.GiveItem(qi2.Item); award.ItemXp *= 2; NextTick(() => { if (qi2?.Item == null) { return; } InventoryOverlay.UpdateAt(basePlayer, qi2.Item.GetRootContainer(), qi2.Item.position, false); }); } skills[qi.ShortName].GrantXP(award.ItemXp); skills[qi.Category].GrantXP(award.CategoryXp); skills[qi.ShortName].GrantXP(award.ItemXp, (levelup) => { Tracking.Refresh(basePlayer, Tracking.Get(basePlayer.userID, qi.ShortName), levelup, award.ItemXp); }); skills[qi.Category].GrantXP(award.CategoryXp, (levelup) => { Tracking.Refresh(basePlayer, Tracking.Get(basePlayer.userID, qi.Category), levelup, award.CategoryXp); if (levelup) { Leaderboards.RefreshPlayerRankings(category); } }); if (qi.Quality > 0) { Notifications.Show(basePlayer, qi, duplicated); } // Stats if (CONFIG.SkillsMenu.ShowStatistics) { var stats = Statistics.Get(basePlayer.userID).Categories[category]; stats.NumItemsCrafted++; if (qi.Quality > 0) { stats.NumQualityItemsCrafted++; } if (duplicated) { stats.ItemsDuplicated++; } if (category == CraftingCategoryType.Clothing) { if (stats.ClothUsed == null) { stats.ClothUsed = 0; } stats.ClothUsed += (int)(task.blueprint.ingredients.FirstOrDefault(x => x.itemDef.shortname == "cloth")?.amount ?? 0); } if (category == CraftingCategoryType.Firearms) { if (stats.SpringsUsed == null) { stats.SpringsUsed = 0; } stats.SpringsUsed += (int)(task.blueprint.ingredients.FirstOrDefault(x => x.itemDef.shortname == "metalspring")?.amount ?? 0); } if (category == CraftingCategoryType.Melee) { if (stats.MetalBladesUsed == null) { stats.MetalBladesUsed = 0; } stats.MetalBladesUsed += (int)(task.blueprint.ingredients.FirstOrDefault(x => x.itemDef.shortname == "metalblade")?.amount ?? 0); } if (category == CraftingCategoryType.Bows) { if (stats.RopeUsed == null) { stats.RopeUsed = 0; } stats.RopeUsed += (int)(task.blueprint.ingredients.FirstOrDefault(x => x.itemDef.shortname == "rope")?.amount ?? 0); } stats.TotalCraftTime += task.blueprint.time; } } } } namespace Oxide.Plugins { public partial class QualityCrafting { public static class CSV { public static string CsvPath => Interface.Oxide.DataDirectory + $"/{INSTANCE.Name}/v3/Tables/"; private static string FormatFileName(string filename) { if (!filename.EndsWith(".csv")) { filename += ".csv"; } return filename; } public static bool Exists(string filename) { Directory.CreateDirectory(CsvPath); return File.Exists(CsvPath + FormatFileName(filename)); } public static void SaveCsv(string filename, string data) { Directory.CreateDirectory(CsvPath); File.WriteAllText(CsvPath + FormatFileName(filename), data); } public static CsvNumberTable LoadCsv(string filename) { Directory.CreateDirectory(CsvPath); var path = CsvPath + FormatFileName(filename); string data = string.Empty; if (File.Exists(path)) { data = File.ReadAllText(path); } else { INSTANCE.PrintWarning($"CSV file {filename} does not exist within {CsvPath}"); } return new CsvNumberTable(data); } } public class CsvNumberTable { public CsvNumberTable(float[][] data) { _data = data; } public CsvNumberTable(string data) { _data = data.Split('\n') .Select(x => x .Split(',') .Select(y => { float parsed; if (float.TryParse(y, out parsed)) { return parsed; } INSTANCE.PrintWarning($"Failed to parse value in CSV, value = '{y}'"); return 0f; }) .ToArray()) .ToArray(); } private float[][] _data { get; set; } public float Get(int row, int col) { return _data[row-1][col-1]; } public override string ToString() { return string.Join('\n', _data.Select(x => string.Join(',', x))); } } } } namespace Oxide.Plugins { public partial class QualityCrafting { /* These are examples of hooks other plugins can subscribe to */ //private void QC_SkillMenuOpened(BasePlayer basePlayer) //{ // Puts("QC_SkillMenuOpened"); //} //private void QC_SkillMenuClosed(BasePlayer basePlayer) //{ // Puts("QC_SkillMenuClosed"); //} //private void QC_ItemInspectionOpened(BasePlayer basePlayer, Item item) //{ // Puts("QC_ItemInspectionOpened"); //} //private void QC_ItemInspectionClosed(BasePlayer basePlayer) //{ // Puts("QC_ItemInspectionClosed"); //} //private float QC_GetCraftingSpeedModifier(BasePlayer basePlayer) //{ // return 0f; //} //private void QC_ItemMovedToContainer(BasePlayer basePlayer, ItemContainer newContainer, Item item) //{ //} //private void QC_ItemMovedToInventory(BasePlayer basePlayer, ItemContainer sourceContainer, ItemContainer destContainer, Item item) //{ //} } } namespace Oxide.Plugins { public partial class QualityCrafting { public static class HudButtons { private const string ID_Quality = "qc.hud.quality"; private const string ID_Skills = "qc.hud.skills"; public static void ShowSkillsButton(BasePlayer basePlayer) { var cfg = CONFIG.HudButtons.SkillsButton; if (!cfg.Show) { return; } var isSprite = cfg.IconUrl.IsAssetPath(); var icon = "hudbutton.skills"; int Size = CONFIG.HudButtons.SkillsButton.Size; var b = new UI { ID = ID_Skills, Parent = "Hud.Menu", Origin = Origin.LowerCenter, Width = Size, Height = Size, //Image = isSprite ? null : icon, //ImageColor = Color(cfg.Color), //ButtonSprite = isSprite ? icon : null, //ButtonSpriteColor = Color(cfg.Color), ButtonImage = icon, ButtonImageColor = Color(cfg.Color), Position = P4(right: cfg.X, up: cfg.Y), Button = ClickAction((args) => { SkillsMenu.Show(basePlayer); }), Children = { !CONFIG.HudButtons.SkillsButton.ShowText ? null :new UI { Origin = Origin.LowerCenter, Height = 16, Width = 100, GrowY = Growth.Negative, Position = P4(down: 6), Text = cfg.ShowText ? basePlayer.Lang("button skills").ToUpper() : null, TextSize = CONFIG.HudButtons.SkillsButton.TextSize, TextAlign = TextAnchor.UpperCenter } } }; SCOPE.Player(basePlayer).Show(b); } public static void ShowQualityButton(BasePlayer basePlayer) { var cfg = CONFIG.HudButtons.QualityButton; if (!cfg.Show) { return; } var isSprite = cfg.IconUrl.IsAssetPath(); var icon = "hudbutton.quality"; int Size = CONFIG.HudButtons.QualityButton.Size; var b = new UI { ID = ID_Quality, Parent = "Hud.Menu", Origin = Origin.LowerCenter, Width = Size, Height = Size, //Image = isSprite ? null : icon, //ImageColor = Color(cfg.Color), ButtonImage = icon, ButtonImageColor = Color(cfg.Color), Position = P4(right: cfg.X, up: cfg.Y), Button = ClickAction((args) => { InventoryOverlay.UpdateAll(basePlayer, basePlayer.inventory.containerBelt, false); InventoryOverlay.UpdateAll(basePlayer, basePlayer.inventory.containerMain, false); InventoryOverlay.UpdateAll(basePlayer, basePlayer.inventory.containerWear, false); var entity = InventoryOverlay.GetViewingEntity(basePlayer); if (entity != null) { foreach(var inventory in entity.GetInventories()) { InventoryOverlay.UpdateAll(basePlayer, inventory, false); } } }), Children = { !CONFIG.HudButtons.QualityButton.ShowText ? null : new UI { Origin = Origin.LowerCenter, Height = 16, Width = 100, GrowY = Growth.Negative, Position = P4(down: 6), Text = cfg.ShowText ? basePlayer.Lang("button quality").ToUpper() : null, TextSize = CONFIG.HudButtons.QualityButton.TextSize, TextAlign = TextAnchor.UpperCenter } } }; SCOPE.Player(basePlayer).Show(b); } public static void Close(BasePlayer basePlayer) { SCOPE?.Player(basePlayer).Destroy(ID_Quality); SCOPE?.Player(basePlayer).Destroy(ID_Skills); } } } } namespace Oxide.Plugins { public partial class QualityCrafting : CovalencePlugin { public static class Inspection { private static readonly string ID = "qc.inspection"; private const int PanelW = 180; private const int PanelCenterW = 240; public const int ImageS = 150; public const int ImageOffset = 16; public static readonly string ShadowColor = "0 0 0 0.95"; private static readonly UiColor PanelColor = COLOR.Rust.DarkBrown; private static readonly UiColor EntryColor = COLOR.Rust.DarkGray; private static readonly UiColor EntryHeaderColor = COLOR.Rust.Gray; private static readonly string PanelMaterial = MATERIAL.UIMaskClear; private const int EntryHeaderHeight = 24; private const int EntryPadding = 6; private const int EntryAfterPadding = 6; private const int StatTrim = 2; private const int SubstatTrim = 24; private const int SubstatGap = 1; private const int SubstatHeight = 22; private static readonly UiColor FontColor = COLOR.Rust.LightBrown; private static readonly UiColor FontHighlightColor = COLOR.Rust.Lime; private const int TextSize = 11; private const int FontSubSize = 9; private const int EntryImageSize = 16; private const int PanelGap = 16; private const int StarSize = 32; private const float StarPercent = 0.35f; private const float LabelSaturation = 0.5f; private const int PanelTitleGap = 6; private const int PanelTitleHeight = 28; private const int PanelTitleTextSize = 14; private static readonly UiColor PanelTitleFontColor = COLOR.Rust.LightGray; private static readonly UiColor DefectColor = COLOR.Rust.Red; private static readonly UiColor DefectTitleColor = COLOR.Rust.LightOrange; private static readonly UiColor DefectTextColor = COLOR.Rust.Red; private struct StatEntry { public string Label { get; set; } public string Value { get; set; } public float Number { get; set; } public string Sprite { get; set; } public bool Show { get; set; } public bool Highlight { get; set; } public int IconAdjust { get; set; } public List SubEntries { get; set; } } public static void ShowInspection(BasePlayer basePlayer, QualityItem qi) { var rowHeightOverrides = new Dictionary(); var rowIndex = 0; var ui = SCOPE.Player(basePlayer); var b = new UI { ID = ID, Parent = "Overlay", NeedsCursor = true, Material = MATERIAL.UIBackgroundBlurInGameMenu, Color = Color("0 0 0 0.95"), Button = ClickAction((args) => { CloseInspection(basePlayer); }), Children = { //!qi.IsLegendary ? null : new UI //{ // FadeIn = 0.5f, // FadeOut = 3f, // Duration = 1f, // Image = GetImage($"legendary.{qi._legendaryID}.icon"), // ImageType = ImageType.Raw, // ImageColor = COLOR.White.Set(a: 0.02f) //}, //!qi.IsLegendary ? null : new UI //{ // FadeIn = 1, // Image = GetImage($"overlay-zmb"), // ImageType = ImageType.Raw, // ImageColor = COLOR.White.Set(a: 0.01f) //}, new UI { Origin = D4(0.25f, 0.35f, 0.75f, 0.75f), FadeIn = 0.3f, FadeOut = 0.3f, Children = { new UI { Origin = Origin.ColLeft, Width = PanelW, Color = PanelColor, Material = PanelMaterial, Children = { new UI { Color = PanelColor, Position = P4(up: PanelTitleGap), Origin = Origin.RowUpper, Height = PanelTitleHeight, Text = basePlayer.Lang("inspection perks").ToUpper(), TextAlign = TextAnchor.MiddleCenter, TextSize = PanelTitleTextSize, TextColor = PanelTitleFontColor }, new UI { Enabled = !qi.AnyPerks, Text = basePlayer.Lang("inspection no perks"), TextColor = FontColor, TextAlign = TextAnchor.MiddleCenter, TextSize = TextSize }, new UI { Enabled = qi.AnyPerks, Trim = D4(top: PanelTitleHeight), Layout = new UiLayout { Rows = 5 }, Children = UI.ForEach(qi.Perks.OrderByDescending(x => x.Perk.IsNegative).ThenByDescending(x => x.Rank), (perk) => { return new UI { Trim = D4(4), Color = EntryColor, Children = { new UI { Color = perk.Perk.IsNegative ? DefectColor : Color(perk.Perk.Color), Height = EntryHeaderHeight, GrowY = Growth.Negative, Origin = Origin.RowUpper, Children = { new UI { Position = D4(right: EntryPadding), Height = EntryImageSize, Width = EntryImageSize, Origin = Origin.MiddleLeft, Image = perk.Perk.IconID, ImageType = ImageType.Simple, ImageColor = perk.Perk.IsNegative ? DefectTitleColor : Color(perk.Perk.Color).Saturate(LabelSaturation) }, new UI { Trim = D4(left: EntryPadding + EntryImageSize + EntryAfterPadding), Text = $"{perk.Perk.NameLocalized(basePlayer)} {perk.RankText}", TextAlign = TextAnchor.MiddleLeft, TextColor = perk.Perk.IsNegative ? DefectTitleColor : Color(perk.Perk.Color).Saturate(LabelSaturation), TextSize = TextSize }, !perk.Perk.IsNegative ? null : new UI { Trim = D4(right: EntryPadding), Text = basePlayer.Lang("defect").ToUpper(), TextAlign = TextAnchor.MiddleRight, TextColor = COLOR.White.Set(a: 0.2f), TextSize = TextSize } } }, new UI { Trim = D4(top: EntryHeaderHeight, left: EntryPadding, right: EntryPadding), Text = perk.Perk.DescriptionLocalized(basePlayer), TextColor = perk.Perk.IsNegative ? DefectTextColor : FontColor, TextSize = FontSubSize, TextAlign = TextAnchor.MiddleLeft } } }; }) } } }, new UI { Origin = Origin.ColCenter, Width = PanelCenterW, Height = PanelTitleGap + PanelTitleHeight, Children = { !qi.IsLegendary ? null : new UI { FadeIn = 1, Image = $"legendary.{qi._legendaryID}.icon", ImageType = ImageType.Raw, ImageColor = COLOR.White.Set(a: 0.5f), Origin = Origin.RowUpper, Height = 32 }, qi.IsLegendary ? null : new UI { FadeIn = 1, Image = "banner", ImageType = ImageType.Raw, ImageColor = qi.QualityColor.Set(a: 0.5f), Origin = Origin.RowUpper, Height = 32 }, new UI { Height = 40, GrowY = Growth.Negative, Position = P4(down: EntryPadding), Origin = Origin.RowUpper, Text = qi.DisplayName, TextAlign = TextAnchor.UpperCenter, TextSize = 18, TextColor = qi.QualityColor.Saturate(0.5f) }, new UI { Height = 40, GrowY = Growth.Negative, Position = P4(down: EntryPadding+32), Origin = Origin.RowUpper, Trim = D4(left: EntryPadding, right: EntryPadding), Layout = new UiLayout { Cols = 5 }, Children = UI.For(5, (i) => { var colored = i < qi.Quality; return new UI { Width = StarSize, Height = StarSize, Origin = Origin.MiddleCenter, Image = "star", ImageType = ImageType.Simple, ImageColor = colored ? Color("1 0.8 0 1") : Color("0.1 0.1 0.1 1") }; }) }, new UI { Height = 40, Position = P4(up: PanelGap), Origin = Origin.RowUpper, GrowY = Growth.Positive, Text = basePlayer.Lang("inspection title"), TextAlign = TextAnchor.LowerCenter, TextSize = 24 }, new UI { Height = 40, Width = 500, GrowY = Growth.Negative, Position = P4(down: PanelGap+20), Origin = Origin.LowerCenter, Text = qi.Description, TextAlign = TextAnchor.UpperCenter, TextSize = 16, TextColor = Color("1 1 1 0.5") }, new UI { Origin = Origin.MiddleCenter, Height = ImageS, Width = ImageS, Position = P4(down: ImageOffset), Image = qi.Item.info.itemid, ImageSkinId = qi.Item.skin, ImageType = ImageType.Item } } }, new UI { Origin = Origin.ColRight, GrowX = Growth.Negative, Width = PanelW, Color = PanelColor, Material = PanelMaterial, Children = { new UI { Color = PanelColor, Position = P4(up: PanelTitleGap), Origin = Origin.RowUpper, Height = PanelTitleHeight, Text = basePlayer.Lang("inspection stats").ToUpper(), TextAlign = TextAnchor.MiddleCenter, TextSize = PanelTitleTextSize, TextColor = PanelTitleFontColor }, new UI { Trim = D4(top: PanelTitleHeight), Layout = new UiLayout { Rows = 12, RowHeight = 30, RowHeights = rowHeightOverrides }, Children = UI.ForEach(new StatEntry[] { new StatEntry { Sprite = SPRITE.Warning2, Label = basePlayer.Lang("inspection stat defect"), Value = (((float?)Interface.CallHook("QualityCrafting_GetDefectChance", basePlayer, qi)) ?? 0f).Percent(), Show = ExtendedIsLoaded && qi.Item.IsBlueprint() }, new StatEntry { Label = basePlayer.Lang("inspection stat condition"), Value = $"{qi.Item._condition.Floor()}/{qi.Item._maxCondition.Floor()}", Show = qi.Item.hasCondition, Highlight = qi.HasPerk(Perks.Durable.ID), Sprite = SPRITE.Gear, IconAdjust = -1 }, new StatEntry { Label = basePlayer.Lang("inspection stat damage scale"), //Value = $"{qi.GetHeldEntity()?.damageScale.Percent()}", Value = qi.DamageScale.Percent(), Show = qi.Category == CraftingCategoryType.Bows || qi.Category == CraftingCategoryType.Firearms, Highlight = qi.DamageScale > 1f, Sprite = SPRITE.Weapon }, new StatEntry { Label = basePlayer.Lang("inspection stat damage"), //Value = $"{qi.GetHeldEntity()?.TotalDamage()}", //Value = $"{qi.GetHeldEntity()?.TotalDamage()*qi.DamageScale}", Value = FormatStatWithModifier(qi.BaseDamage.Floor(), qi.BonusDamage.Floor()), Show = qi.Category == CraftingCategoryType.Melee || qi.Category == CraftingCategoryType.Tools, //Highlight = qi.DamageScale > 1f, Highlight = false, Sprite = SPRITE.Weapon }, new StatEntry { Label = basePlayer.Lang("inspection stat protection"), Show = qi.Category == CraftingCategoryType.Clothing, Highlight = false, Sprite = SPRITE.Clothing, SubEntries = QualityItem.GetClothingProtectionStats()?.Where(x => !(new Rust.DamageType[] { // Exclusions Rust.DamageType.Slash }.Contains(x.DamageType))).Select(x => new StatEntry { Sprite = x.Sprite, Label = basePlayer.Lang(x.TextId), //Highlight = qi.GetBonusResistance(x.DamageType) > 0f, Highlight = false, Value = FormatStatWithModifier( (ScaleDamageTypeValue(x.DamageType, qi.GetBaseResistance(x.DamageType))*100).Floor(), (ScaleDamageTypeValue(x.DamageType, qi.GetBonusResistance(x.DamageType))*100).Floor(), format: "{0}% ({1})", singleFormat: "{0}%" ) //Value = ScaleDamageTypeValue(x.DamageType, qi.GetTotalProtection(x.DamageType)).Percent() }).Where(x => x.Value != 0f.Percent()).ToList() }, new StatEntry { Label = basePlayer.Lang("inspection stat gathering"), Show = qi.Category == CraftingCategoryType.Melee || qi.Category == CraftingCategoryType.Tools, Highlight = false, Sprite = SPRITE.Tools, SubEntries = new List{ new StatEntry { Sprite = SPRITE.LevelWood, Label = basePlayer.Lang("inspection stat wood"), //Value = $"{qi.GetHeldEntity()?.gathering.Tree.gatherDamage.Floor()}", Value = FormatStatWithModifier(qi.BaseGather(ResourceDispenser.GatherType.Tree).Floor(), qi.BonusGather(ResourceDispenser.GatherType.Tree).Floor()), Highlight = false //Highlight = qi.HasPerk(Perks.Efficient.ID) }, new StatEntry { Sprite = SPRITE.LevelStone, Label = basePlayer.Lang("inspection stat ore"), //Value = $"{qi.GetHeldEntity()?.gathering.Ore.gatherDamage.Floor()}", Value = FormatStatWithModifier(qi.BaseGather(ResourceDispenser.GatherType.Ore).Floor(), qi.BonusGather(ResourceDispenser.GatherType.Ore).Floor()), Highlight = false //Highlight = qi.HasPerk(Perks.Efficient.ID) }, new StatEntry { Sprite = SPRITE.Meat, Label = basePlayer.Lang("inspection stat flesh"), Value = FormatStatWithModifier(qi.BaseGather(ResourceDispenser.GatherType.Flesh).Floor(), qi.BonusGather(ResourceDispenser.GatherType.Flesh).Floor()), Highlight = false //Value = $"{qi.GetHeldEntity()?.gathering.Flesh.gatherDamage.Floor()}", //Highlight = qi.HasPerk(Perks.Efficient.ID) } } } }.Where(x => x.Show), (stat) => { var vals = new List(); var u = new UI { Trim = D4(StatTrim), Color = EntryColor, Children = { new UI { Position = D4(right: EntryPadding), Origin = Origin.MiddleLeft, Height = EntryImageSize+stat.IconAdjust, Width = EntryImageSize+stat.IconAdjust, Image = stat.Sprite, ImageColor = FontColor, ImageType = ImageType.Sprite }, new UI { Trim = D4(left:EntryPadding+EntryImageSize+EntryAfterPadding, bottom:EntryPadding, right:EntryPadding,top:EntryPadding), Text = stat.Label, TextColor = FontColor, TextSize = TextSize, TextAlign = TextAnchor.MiddleLeft }, new UI { Enabled = !string.IsNullOrWhiteSpace(stat.Value), Trim = D4(left:EntryPadding+EntryImageSize+EntryAfterPadding, bottom:EntryPadding, right:EntryPadding,top:EntryPadding), TextColor = stat.Highlight ? FontHighlightColor : FontColor, TextSize = TextSize, Text = stat.Value, TextAlign = TextAnchor.MiddleRight } } }; rowIndex++; vals.Add(u); if (stat.SubEntries != null) { foreach(var substat in stat.SubEntries) { rowHeightOverrides[rowIndex] = SubstatHeight; rowIndex++; vals.Add(new UI { Trim = D4(left: SubstatTrim, top: SubstatGap, bottom: SubstatGap, right: StatTrim), Color = EntryColor, Children = { new UI { Position = D4(right: EntryPadding), Origin = Origin.MiddleLeft, Inset = D4(2), Height = EntryImageSize+substat.IconAdjust, Width = EntryImageSize+substat.IconAdjust, Image = substat.Sprite, ImageType = ImageType.Sprite, ImageColor = FontColor }, new UI { Trim = D4(left:EntryPadding+EntryImageSize+EntryAfterPadding, right:EntryPadding), TextColor = FontColor, TextSize = TextSize-2, Text = substat.Label, TextAlign = TextAnchor.MiddleLeft }, new UI { Trim = D4(right:EntryPadding), TextColor = substat.Highlight ? FontHighlightColor : FontColor, TextSize = TextSize-2, Text = substat.Value, TextAlign = TextAnchor.MiddleRight } } }); } } return vals; }) } } }, } } } }; ui.Show(b); Interface.CallHook("QC_ItemInspectionOpened", basePlayer, qi.Item); } public static void CloseInspection(BasePlayer basePlayer) { SCOPE?.Player(basePlayer).Destroy(ID); Interface.CallHook("QC_ItemInspectionClosed", basePlayer); } } } } namespace Oxide.Plugins { public partial class QualityCrafting : CovalencePlugin { [Command("qc.inspect")] private void CmdInspect(IPlayer player, string command, string[] args) { // [0] = itemid [1] = containerid BasePlayer basePlayer = player.BasePlayer(); if (basePlayer == null) { return; } ulong itemuid = ulong.Parse(args[0]); string specId = args[1]; var container = GetContainer(basePlayer, specId); if (container == null) { return; } var item = container.itemList.FirstOrDefault(x => x.uid.Value == itemuid); if (item == null) { return; } var qi = item.QualityIfExists(); if (qi?.Quality <= 0) { return; } Inspection.ShowInspection(basePlayer, qi); } public static class InventoryOverlay { public const string Layer = "Hud.Menu"; private static List ElementsToBuild = new List(); private static List ElementsToDestroy = new List(); private static void ResetBuild() { ElementsToBuild.Clear(); ElementsToDestroy.Clear(); } public enum VendingMachineMode { Shop, Admin } public static readonly OverlayContainerSpecs InventoryHotbar = new OverlayContainerSpecs { id = "hotbar", left = -199.5f, bottom = 18, size = 60, gap = 4, rows = 1, cols = 6, sections = 1 }; public static readonly OverlayContainerSpecs InventoryMain = new OverlayContainerSpecs { id = "main", left = -199.5f, bottom = 87.75f, size = 60, gap = 4, rows = 4, cols = 6, sections = 1 }; public static readonly OverlayContainerSpecs InventorySmallBackpack = new OverlayContainerSpecs { id = "backpack", left = -620f, bottom = 472.5f, size = 56, gap = 4, rows = 3, cols = 4, sections = 1 }; public static readonly OverlayContainerSpecs InventoryLargeBackpack = new OverlayContainerSpecs { id = "backpack", left = -620f, bottom = 232.5f, size = 56, gap = 4, rows = 7, cols = 4, sections = 1 }; public static readonly OverlayContainerSpecs InventoryWear = new OverlayContainerSpecs { id = "wear", left = -586f, bottom = 116.75f, size = 49, gap = 5, rows = 1, cols = 7, sections = 1 }; public static readonly OverlayContainerSpecs InventoryStorage = new OverlayContainerSpecs { id = "storage", left = 198.5f, bottom = 111.5f, size = 58, gap = 4, rows = 8, cols = 6, sections = 1 }; public static readonly OverlayContainerSpecs StorageLocker = new OverlayContainerSpecs { id = "storage", left = 196f, bottom = 139f, size = 49, gap = 5, rows = 6, cols = 7, sections = 3, sectiongap = 31f }; public static readonly OverlayContainerSpecs VendingMachineShop = new OverlayContainerSpecs { id = "storage", left = 210f, bottom = 115.5f, size = 60, gap = 14, rows = 1, cols = 1, sections = 1 }; public static readonly OverlayContainerSpecs VendingMachineAdmin = new OverlayContainerSpecs { id = "storage", left = 198.5f, bottom = 111.5f, size = 58, gap = 4, rows = 5, cols = 6, sections = 1 }; public static readonly OverlayContainerSpecs RepairBench = new OverlayContainerSpecs { id = "storage", left = 184f, bottom = 128.5f, size = 128, gap = 4, rows = 1, cols = 1, sections = 1 }; public static readonly OverlayContainerSpecs ResearchTable = new OverlayContainerSpecs { id = "storage", left = 192.5f, bottom = 377.5f, size = 128, gap = 4, rows = 1, cols = 1, sections = 1 }; public static readonly OverlayContainerSpecs IndustrialCrafter = new OverlayContainerSpecs { id = "storage", left = 260.5f, bottom = 163.5f, size = 58, rows = 3, cols = 4, gap = 4, sections = 3, sectiongaps = new float[] { 50, 24 }, }; public static readonly OverlayContainerSpecs CorpseBelt = new OverlayContainerSpecs { id = "corpsebelt", left = 198.5f, bottom = 111.5f, size = 58, gap = 4, rows = 1, cols = 6, sections = 1 }; public static readonly OverlayContainerSpecs CorpseWear = new OverlayContainerSpecs { id = "corpsewear", left = 202.5f, bottom = 202f, size = 48, gap = 4, rows = 2, cols = 7, sections = 1 }; public static readonly OverlayContainerSpecs CorpseMain = new OverlayContainerSpecs { id = "corpsemain", left = 198.5f, bottom = 336f, size = 58, gap = 4, rows = 4, cols = 6, sections = 1 }; private static readonly OverlayContainerSpecs[] AllSpecs = new OverlayContainerSpecs[] { InventoryHotbar, InventoryMain, InventoryWear, InventoryStorage, InventorySmallBackpack, CorpseBelt, CorpseWear, CorpseMain, StorageLocker, VendingMachineAdmin, VendingMachineShop }; public struct OverlayContainerSpecs { public string id; public float left; public float bottom; public float size; public float gap; public int rows; public int cols; public int sections; public float sectiongap; public float[] sectiongaps; public float Top => bottom + TotalH; public float Right => left + TotalW; public float TotalW => (size * cols) + ((cols - 1) * gap); public float TotalH => (size * rows) + ((rows - 1) * gap) + (sections <= 1 ? 0 : sectiongaps != null ? sectiongaps.Sum() : ((sections-1) * sectiongap)); public string SlotID(int index) => $"{id}#{index}"; public Dim4 GetSlot(int index) { var capacity = rows * cols; var section = 0; var sectgap = 0f; var totalsectgap = 0f; if (sections > 1) { var numPerSection = (int)(capacity / (float)sections); section = (int)Math.Floor(index / (float)numPerSection); sectgap = sectiongap; totalsectgap = section * sectgap; if (sectiongaps != null && section > 0) { totalsectgap = sectiongaps.Take(section).Sum(); } } var r = ((int)Math.Floor((float)index / cols)); var c = (int)(index % cols); var l = c == 0 ? 0 : (size * c) + ((gap - 1) * c) + c; var t = TotalH - (r == 0 ? 0 : (size * r) + ((gap - 1) * r)) - r - (totalsectgap); return D4( left: l, top: t, right: l+size, bottom: t-size ); } } public struct OverlayInfo { public bool HotbarIsShown; public bool MainIsShown; public bool WearIsShown; public bool BackpackIsShown; public BaseCombatEntity ViewingEntity; public bool StorageIsShown; public VendingMachineMode VendingMachineMode; public bool HasAnyExceptHotbar => MainIsShown || WearIsShown || StorageIsShown || BackpackIsShown; public ItemContainer Inventory => ViewingEntity?.GetInventory(); public void SetShownBySpec(string id) { if (id == InventoryHotbar.id) { HotbarIsShown = true; } else if (id == InventoryMain.id) { MainIsShown = true; } else if (id == InventoryWear.id) { WearIsShown = true; } else if (id == InventorySmallBackpack.id) { BackpackIsShown = true; } else { StorageIsShown = true; } } public bool GetBySpec(string id) { if (id == InventoryHotbar.id) { return HotbarIsShown; } else if (id == InventoryMain.id) { return MainIsShown; } else if (id == InventoryWear.id) { return WearIsShown; } else if (id == InventorySmallBackpack.id) { return BackpackIsShown; } else { return StorageIsShown; } } } public static Dictionary PlayerOverlays = new Dictionary(); public static bool HasOverlay(BasePlayer basePlayer, string id) => PlayerOverlays.ContainsKey(basePlayer.userID) && PlayerOverlays[basePlayer.userID].GetBySpec(id); public static bool HasAnyOverlayExceptHotbar(BasePlayer basePlayer) => PlayerOverlays.ContainsKey(basePlayer.userID) && PlayerOverlays[basePlayer.userID].HasAnyExceptHotbar; public static BaseCombatEntity GetViewingEntity(BasePlayer basePlayer) { if (!PlayerOverlays.ContainsKey(basePlayer.userID)) { return null; } var info = PlayerOverlays[basePlayer.userID]; return info.ViewingEntity; } public static void SetViewingEntity(BasePlayer basePlayer, BaseCombatEntity entity) { var info = PlayerOverlays.GetValueOrNew(basePlayer.userID); info.ViewingEntity = entity; PlayerOverlays[basePlayer.userID] = info; } public static void SetVendingMachineMode(BasePlayer basePlayer, VendingMachineMode mode) { var info = PlayerOverlays.GetValueOrNew(basePlayer.userID); info.VendingMachineMode = mode; PlayerOverlays[basePlayer.userID] = info; } private static bool VendingMachineModeEquals(BasePlayer basePlayer, VendingMachineMode mode) { if (!PlayerOverlays.ContainsKey(basePlayer.userID)) { return false; } var info = PlayerOverlays[basePlayer.userID]; return info.VendingMachineMode == mode; } public static void UpdateAll(BasePlayer basePlayer, ItemContainer container, bool forceInit = false) { var specs = GetSpecs(basePlayer, container); var ui = SCOPE.Player(basePlayer); if (forceInit || !HasOverlay(basePlayer, specs.id)) { InitOverlay(basePlayer, container); } if (container.entityOwner is VendingMachine vendingMachine && VendingMachineModeEquals(basePlayer, VendingMachineMode.Shop)) { for (int i = 0; i < vendingMachine.sellOrders.sellOrders.Count; i++) { var sellOrder = vendingMachine.sellOrders.sellOrders[i]; var item = container.itemList.FirstOrDefault(x => sellOrder.itemToSellID == x.info.itemid); if (!item.IsQualityItem()) { continue; } var qi = item.QualityIfExists(); if (qi?.Quality <= 0) { continue; } AddElementsToList(basePlayer, container, specs, i, qi); } } else { for (int i = 0; i < container.capacity; i++) { var item = container.GetSlot(i); if (!item.IsQualityItem()) { continue; } AddElementsToList(basePlayer, container, specs, i); } } ui.Destroy(ElementsToDestroy); ui.Show(ElementsToBuild); ResetBuild(); } public static void UpdateAt(BasePlayer basePlayer, ItemContainer container, int index, bool initIfNeeded) { if (basePlayer == null || container == null) { return; } if (index < 0) { return; } var specs = GetSpecs(basePlayer, container); var entity = container.entityOwner; if (entity is RepairBench) { InitOverlay(basePlayer, container); } else if (!HasOverlay(basePlayer, specs.id)) { if (!initIfNeeded) { return; } UpdateAll(basePlayer, container); // If the container hasn't been drawn yet, draw everything in it return; } var ui = SCOPE.Player(basePlayer); AddElementsToList(basePlayer, container, specs, index); ui.Destroy(ElementsToDestroy); ui.Show(ElementsToBuild); ResetBuild(); if (specs.id == InventoryWear.id) { ClothingStatsOverlay.Update(basePlayer); } } public static void DestroyAllButHotbar(BasePlayer basePlayer) { foreach(var spec in AllSpecs) { if (spec.id == InventoryHotbar.id) { continue; } SCOPE?.Player(basePlayer).Destroy(spec.id); } var info = PlayerOverlays.GetValueOrNew(basePlayer.userID); info.MainIsShown = false; info.WearIsShown = false; info.StorageIsShown = false; PlayerOverlays[basePlayer.userID] = info; ClothingStatsOverlay.SetSelectedLayer(basePlayer.userID, null); } public static void DestroyAll(BasePlayer basePlayer) { foreach(var spec in AllSpecs) { SCOPE?.Player(basePlayer).Destroy(spec.id); } PlayerOverlays.Remove(basePlayer.userID); ClothingStatsOverlay.SetSelectedLayer(basePlayer.userID, null); } public static void Destroy(BasePlayer basePlayer, string id) { SCOPE?.Player(basePlayer).Destroy(id); var info = PlayerOverlays.GetValueOrNew(basePlayer.userID); info.SetShownBySpec(id); PlayerOverlays[basePlayer.userID] = info; } private static ItemContainer GetSubContainer(ItemContainer container) { foreach(var item in container.itemList) { if (item.contents != null) { return item.contents; } } return null; } public static OverlayContainerSpecs GetSpecs(BasePlayer basePlayer, ItemContainer container) { if (container.parent?.info.shortname == "smallbackpack") { return InventorySmallBackpack; } if (container.parent?.info.shortname == "largebackpack") { return InventoryLargeBackpack; } if (container.uid == basePlayer.inventory.containerBelt.uid) { return InventoryHotbar; } else if (container.uid == basePlayer.inventory.containerMain.uid) { return InventoryMain; } else if (container.uid == basePlayer.inventory.containerWear.uid) { return InventoryWear; } var entity = container.GetEntityOwner(); if (entity is PlayerCorpse || entity is BasePlayer) { if (container.capacity == 24) { return CorpseMain; } else if (container.capacity == 6) { return CorpseBelt; } else { return CorpseWear; } } else if (entity is ResearchTable) { return ResearchTable; } else if (entity is IndustrialCrafter) { return IndustrialCrafter; } else if (entity is Locker) { return StorageLocker; } else if (entity is VendingMachine vendingMachine) { if (VendingMachineModeEquals(basePlayer, VendingMachineMode.Shop)) { var specs = VendingMachineShop; specs.rows = vendingMachine.sellOrders.sellOrders.Count; return specs; } else { return VendingMachineAdmin; } } else if (entity is RepairBench repairBench) { var specs = RepairBench; var item = repairBench.inventory.GetSlot(0); if (item != null && (item.info.HasSkins || (item.info.isRedirectOf != null))) { specs.bottom = 292.5f; } return specs; } var storage = InventoryStorage; if (entity is DroppedItemContainer) { storage.rows = 6; } else { storage.rows = (int)(container.capacity / (float)storage.cols); } return storage; } public static ItemContainer GetContainer(BasePlayer basePlayer, string specId) { if (specId == InventoryHotbar.id) { return basePlayer.inventory.containerBelt; } if (specId == InventoryMain.id) { return basePlayer.inventory.containerMain; } if (specId == InventoryWear.id) { return basePlayer.inventory.containerWear; } if (specId == InventorySmallBackpack.id) { return GetSubContainer(basePlayer.inventory.containerWear); } if (!PlayerOverlays.ContainsKey(basePlayer.userID)) { return null; } var info = PlayerOverlays.GetValueOrDefault(basePlayer.userID); return info.Inventory; } private static void InitOverlay(BasePlayer basePlayer, ItemContainer container) { var specs = GetSpecs(basePlayer, container); var info = PlayerOverlays.GetValueOrNew(basePlayer.userID); info.SetShownBySpec(specs.id); PlayerOverlays[basePlayer.userID] = info; var ui = SCOPE.Player(basePlayer); var element = new UiElement { Parent = Layer, ID = specs.id, Visible = true, Components = new List() { //new UiImageComponent //{ // Color = COLOR.Red.Set(a: 0.5f) //}, new UiRectTransform { Offsets = D4(specs.left, specs.bottom, specs.Right, specs.Top), Anchors = Origin.LowerCenter } } }; ui.Destroy(specs.id); ui.Show(new UiElement[] { element }); if (specs.id == InventoryWear.id) { ClothingStatsOverlay.Show(basePlayer); } } private static void AddElementsToList(BasePlayer basePlayer, ItemContainer container, OverlayContainerSpecs specs, int index, QualityItem qi = null) { var id = specs.SlotID(index); ElementsToDestroy.Add(id); if (qi == null) { var item = container.GetSlot(index); if (!item.IsQualityItem()) { return; } qi = item.QualityIfExists(); if (qi?.Quality <= 0) { return; } } var dims = specs.GetSlot(index); var scale = CONFIG.InventoryOverlay.QualityStarsSizeScale; var element = new UiElement { Parent = specs.id, ID = id, Visible = true, Components = new List() { new UiRawImageComponent { Png = UiEngine.GetImageData($"star{qi.Quality}"), Color = Const.StarColor }, new UiRectTransform { Offsets = D4(dims.xmax - (specs.size * scale / 2), dims.ymax - (specs.size * scale / 2), dims.xmax, dims.ymax), Anchors = Origin.LowerLeft } } }; var button = new UiElement { ID = $"{id}.button", Parent = id, Visible = true, Components = new List { new UiButtonComponent { Command = $"qc.inspect {qi.Item.uid} {specs.id}" } } }; ElementsToBuild.Add(element); if (qi.Perks.Any(x => x.Perk.IsNegative)) { var size = 12; var negelement = new UiElement { Parent = id, ID = $"{id}.negative", Visible = true, Components = new List() { new UiImageComponent { Sprite = SPRITE.Electric, Color = COLOR.Rust.Red }, new UiRectTransform { Anchors = Origin.UpperRight, Offsets = D4(-size*2, -size*2, 0, 0) } } }; ElementsToBuild.Add(negelement); } ElementsToBuild.Add(button); } } } } namespace Oxide.Plugins { public partial class QualityCrafting { public struct LeaderboardEntry { public ulong UserID { get; set; } public int Level { get; set; } } public static class Leaderboards { public static Dictionary> CategoryRankings = new Dictionary>(); public static void InitPlayerRankings() { foreach(var category in CraftingCategory.All()) { CategoryRankings[category] = INSTANCE.PlayerCraftingSkills.OrderByDescending(x => x.Value.Categories[category].Level).Select(x => new LeaderboardEntry { Level = x.Value.Categories[category].Level, UserID = x.Key }).ToList(); } } public static void RefreshPlayerRankings(CraftingCategoryType category) { CategoryRankings[category] = INSTANCE.PlayerCraftingSkills.OrderByDescending(x => x.Value.Categories[category].Level).Select(x => new LeaderboardEntry { Level = x.Value.Categories[category].Level, UserID = x.Key }).ToList(); } } } } namespace Oxide.Plugins { public partial class QualityCrafting { public class XpAward { public int CategoryXp { get; set; } public int ItemXp { get; set; } } public Dictionary PlayerCraftingSkills = new Dictionary(); public static class Leveling { public static void LoadPlayerCraftingSkills(string prefix = "v3") { var data = Interface.Oxide.DataFileSystem.ReadObject>($"{INSTANCE.Name}/{prefix}/PlayerCraftingSkills") ?? new Dictionary(); INSTANCE.PlayerCraftingSkills = data; } public static void SavePlayerCraftingSkills(string prefix = "v3") { Interface.Oxide.DataFileSystem.WriteObject($"{INSTANCE.Name}/{prefix}/PlayerCraftingSkills", INSTANCE.PlayerCraftingSkills); } #region Legacy Import private class LegacyPlayerSkill { public ulong UserID { get; set; } public Dictionary CategoryLevels = new Dictionary(); public Dictionary ItemLevels = new Dictionary(); } public static void ImportLegacyPlayerCraftingSkills(Action callback) { var legacySkills = Interface.Oxide.DataFileSystem.ReadObject>($"{INSTANCE.Name}/ImportData") ?? new List(); INSTANCE.PlayerCraftingSkills.Clear(); int success = 0; int fails = 0; if (legacySkills != null) { foreach (var data in legacySkills) { var userId = data.UserID; // Categories foreach (var category in CraftingCategory.All()) { // guns=0, tools=1, tailoring=2, bows=3, weapons=4 int legacyCategoryId; switch (category) { case CraftingCategoryType.Firearms: legacyCategoryId = 0; break; case CraftingCategoryType.Tools: legacyCategoryId = 1; break; case CraftingCategoryType.Clothing: legacyCategoryId = 2; break; case CraftingCategoryType.Bows: legacyCategoryId = 3; break; case CraftingCategoryType.Melee: legacyCategoryId = 4; break; default: continue; } var legacyLevel = Math.Max(0, Math.Min(100, data.CategoryLevels.GetValueOrDefault(legacyCategoryId.ToString()))); INSTANCE.PlayerCraftingSkills.GetValueOrNew(userId)[category].SetLevel(legacyLevel); } // Items foreach (var itemKvp in data.ItemLevels) { var shortname = itemKvp.Key; var legacyLevel = Math.Max(0, Math.Min(100, itemKvp.Value)); INSTANCE.PlayerCraftingSkills.GetValueOrNew(userId)[shortname].SetLevel(legacyLevel); } success++; } } callback.Invoke(success, fails); } #endregion } } } namespace Oxide.Plugins { public partial class QualityCrafting : CovalencePlugin { private void CmdSkills(IPlayer player, string command, string[] args) { var target = (BasePlayer)player.Object; SkillsMenu.Show(target); } public static class SkillsMenu { private const string ID = "qc.menu"; private const string ContentID = "qc.menu.content"; private const string CategoryID = "qc.menu.categories"; private const string CategoryLayoutID = "qc.menu.categories.layout"; private const string CategoryLayoutButtonID = "qc.menu.categories.layout.button"; private const string ListID = "qc.menu.list"; private const string ListTitleID = "qc.menu.list.title"; private const string ListPagID = "qc.menu.pag"; private const string ListPagContentID = "qc.menu.pag.content"; private const string LevelID = "qc.menu.level"; private const string LevelElementID = "qc.menu.levelelement"; private const string LevelContentID = "qc.menu.level.content"; private const string PerksID = "qc.menu.perks"; private const string PerksTitleID = "qc.menu.perks.title"; private const string LeaderboardID = "qc.menu.leaderboard"; private const string LeaderboardTextID = "qc.menu.leaderboard.text"; private const string LeaderboardTitleID = "qc.menu.leaderboard.title"; private const string TrackingID = "qc.menu.tracking"; private const string TrackingTextID = "qc.menu.tracking.text"; private const string ContentLayoutID = "qc.menu.content.layout"; //private static readonly string PanelColor = BetterCUI.COLOR.Rust.Gray; private static readonly UiColor PanelColor = Color(0.7f, 0.7f, 0.65f, 0.05f); private static readonly string PanelMaterial = MATERIAL.UIBackgroundBlur; private static readonly UiColor PanelHighlightColor = COLOR.Rust.LightBlue; private static readonly UiColor HighlightColor = COLOR.Rust.LightBlue; private static readonly UiColor XPColor = COLOR.Rust.LightGreen; private static readonly UiColor XPBackgroundColor = COLOR.Rust.DarkGray; private static readonly UiColor FontColor = COLOR.Rust.LightGray; private const int Width = 900; private const int Height = 600; private const float FadeIn = 0.3f; private const int CloseBtnSize = 16; private const int LeftWidth = 140; private const int TitleHeight = 40; private const int ListGap = 2; private const int Gap = 8; private const int Padding = 8; private const int RightWidth = 200; private const int RightHeaderHeight = 30; private const int CategoryLevelHeight = 80; private const int PerkHeight = 100; private const int PaginationHeight = 40; private const int ContentHeaderHeight = 24; private const int PagIconSize = 24; private const int ItemIconSize = 48; private const int StartIconSize = 16; private const int CategoryIconSize = 16; private const int LargeCategoryIconSize = 32; private const int LeaderBoardFontSize = 12; private static readonly HashSet PlayerOpenedMenu = new HashSet(); private static readonly Dictionary SelectedCategory = new Dictionary(); private static readonly Dictionary SelectedPage = new Dictionary(); public static bool HasMenuOpen(ulong userId) => PlayerOpenedMenu.Contains(userId); private class PageInfo { public int Page { get; set; } = 0; public int MaxPage { get; set; } = 0; public bool EnablePrevious => Page > 0; public bool EnableNext => Page < MaxPage; } //public static void Close(BasePlayer basePlayer) //{ // SelectedCategory.Remove(basePlayer.userID); // SelectedPage.Remove(basePlayer.userID); // PlayerOpenedMenu.Remove(basePlayer.userID); // BetterCUI.OnClickEventAction.ClearByScope(basePlayer.userID, ID); // CuiHelper.DestroyUi(basePlayer, ID); //} private static void ShowLeaderboard(BasePlayer basePlayer, bool textOnly = false) { var ui = SCOPE.Player(basePlayer); if (!textOnly) { var h = new UI { Position = P4(down: CategoryLevelHeight + Gap + RightHeaderHeight + Gap + PerkHeight + Gap), Parent = ContentID, FadeIn = FadeIn, ID = LeaderboardTitleID, Origin = Origin.UpperRight, Height = RightHeaderHeight, Width = RightWidth, Color = PanelColor, Material = PanelMaterial, Text = basePlayer.Lang("menu statistics"), TextAlign = UnityEngine.TextAnchor.MiddleCenter, TextColor = FontColor }; var b = new UI { Trim = D4(top: CategoryLevelHeight + Gap + RightHeaderHeight + Gap + PerkHeight + Gap + RightHeaderHeight + Gap), Parent = ContentID, ID = LeaderboardID, FadeIn = textOnly ? 0 : FadeIn, Origin = Origin.ColRight, Width = RightWidth, Color = PanelColor, Material = PanelMaterial }; ui.Show(h, b); } var category = SelectedCategory.GetValueOrDefault(basePlayer.userID, CraftingCategoryType.Melee); var skills = basePlayer.Skills(); var lvl = skills.Categories[category].Level; if (!CONFIG.SkillsMenu.ShowStatistics || lvl == 0) { var d = new UI { Parent = LeaderboardID, ID = LeaderboardTextID, Text = basePlayer.Lang("menu no statistics to show"), TextAlign = UnityEngine.TextAnchor.MiddleCenter, TextColor = FontColor, TextSize = 10 }; ui.Show(d); return; } var stats = Statistics.Get(basePlayer.userID).Categories[category]; var c = new UI { Parent = LeaderboardID, ID = LeaderboardTextID, Padding = D4(Padding), Layout = new UiLayout { Rows = 10, Cols = 2, ColWidth = 1, ColWidths = new Dictionary { [0] = 140, [1] = 60 } }, Children = Combine( StatElements(basePlayer.Lang("stats items crafted"), stats.NumItemsCrafted), StatElements(basePlayer.Lang("stats quality percent"), stats.PercentQualityItems + "%"), StatElements(basePlayer.Lang("stats crafting rank"), FormatNumberWithSuffix(stats.LeaderboardRank(basePlayer.userID, category))), StatElements(basePlayer.Lang("stats items duplicated"), stats.ItemsDuplicated), StatElements(basePlayer.Lang("stats avg craft time"), stats.AvgCraftTime + " " + basePlayer.Lang("sec")), category != CraftingCategoryType.Clothing ? null : StatElements(basePlayer.Lang("stats cloth consumed"), stats.ClothUsed ?? 0), category != CraftingCategoryType.Firearms ? null : StatElements(basePlayer.Lang("stats springs consumed"), stats.SpringsUsed ?? 0), category != CraftingCategoryType.Firearms ? null : StatElements(basePlayer.Lang("stats headshots"), stats.Headshots ?? 0), category != CraftingCategoryType.Melee ? null : StatElements(basePlayer.Lang("stats metal blades consumed"), stats.MetalBladesUsed ?? 0), category != CraftingCategoryType.Melee ? null : StatElements(basePlayer.Lang("stats damage dealt"), (int) Math.Round(stats.DamageDealt ?? 0, 0)), category != CraftingCategoryType.Tools ? null : StatElements(basePlayer.Lang("stats resources gained"), stats.ResourcesGained ?? 0), category != CraftingCategoryType.Bows ? null : StatElements(basePlayer.Lang("stats rope consumed"), stats.RopeUsed ?? 0), category != CraftingCategoryType.Bows ? null : StatElements(basePlayer.Lang("stats longest hit"), Math.Round(stats.FarthestHit ?? 0, 2) + "m"), category != CraftingCategoryType.Clothing ? null : StatElements(basePlayer.Lang("stats damage taken"), Math.Round(stats.DamageTaken ?? 0, 0)), !ExtendedIsLoaded ? null : StatElements(basePlayer.Lang("stats times refined"), stats.ItemsRefined ?? 0) ) }; ui.Show(c); } private static UI[] StatElements(string label, object value) { return new UI[] { new UI { Text = label + ":", TextAlign = UnityEngine.TextAnchor.UpperLeft, TextColor = FontColor, TextSize = 10 }, new UI { Text = value.ToString(), TextAlign = UnityEngine.TextAnchor.UpperRight, TextColor = FontColor, TextSize = 10 } }; } private static void ShowPerks(BasePlayer basePlayer, bool textOnly = false, bool update = false) { var ui = SCOPE.Player(basePlayer); if (update) { for (int i = 0; i < 3; i++) { ui.Update(PerksID + i, (element) => { var skills = basePlayer.Skills(); element.UpdateComponent((component) => { var category = SelectedCategory.GetValueOrDefault(basePlayer.userID, CraftingCategoryType.Melee); component.FadeIn = 0f; var value = i == 0 ? skills.GetCraftingSpeed(category) : i == 1 ? skills.GetDuplicateChance(category) : i == 2 ? skills.GetRefiningQuality(category) : 0f; component.Text = i == 2 ? ((int)value).ToString() : value.Percent(); return component; }); return element; }); } return; } if (!textOnly) { var h = new UI { Position = P4(down: CategoryLevelHeight + Gap), Parent = ContentID, ID = PerksTitleID, FadeIn = FadeIn, Origin = Origin.UpperRight, Height = RightHeaderHeight, Width = RightWidth, Color = PanelColor, Material = PanelMaterial, Text = basePlayer.Lang("menu bonuses"), TextAlign = UnityEngine.TextAnchor.MiddleCenter, TextColor = FontColor }; ui.Show(h); } var skills = basePlayer.Skills(); var category = SelectedCategory.GetValueOrDefault(basePlayer.userID, CraftingCategoryType.Melee); var b = new UI { Position = P4(down: CategoryLevelHeight + Gap + RightHeaderHeight + Gap), Parent = ContentID, ID = PerksID, FadeIn = textOnly ? 0 : FadeIn, Origin = Origin.UpperRight, Height = PerkHeight, Width = RightWidth, Color = PanelColor, Material = PanelMaterial, Layout = new UiLayout { Rows = 3 }, Children = UI.For(ExtendedIsLoaded ? 3 : 2, (i) => { string label = ""; string value = ""; if (i == 0) { label = basePlayer.Lang("menu crafting speed"); value = skills.GetCraftingSpeed(category).Percent(); }; if (i == 1) { label = basePlayer.Lang("menu duplicate chance"); value = skills.GetDuplicateChance(category).Percent(); } if (i == 2) { label = basePlayer.Lang("menu refining quality"); value = skills.GetRefiningQuality(category).ToString(); } return new UI { Trim = D4(left: Padding, right: Padding), Children = { new UI { Text = $"{label}:", TextAlign = UnityEngine.TextAnchor.MiddleLeft, TextColor = FontColor, TextSize = 12 }, new UI { ID = PerksID + i, Text = $"{value}", TextAlign = UnityEngine.TextAnchor.MiddleRight, TextColor = FontColor, TextSize = 12 } } }; }) }; ui.Show(b); } private static UI LevelElement(BasePlayer basePlayer, int itemid, string name, CraftingCategoryType category, LevelXP skill, int iconW, int padding, int fontSize, int barHeight, bool levelup = false, int change = 0, Dim4? origin = null, UiColor? backgroundColor = null, string id = null) { if (origin == null) { origin = Origin.Full; } var imgw = iconW; var skills = basePlayer.Skills(); var stats = skill; var bardown = (imgw / 2) - (barHeight / 2); var txtup = barHeight + 6; var backgroundColorValue = backgroundColor ?? COLOR.Transparent; id = id ?? Guid.NewGuid().ToString(); return new UI { ID = id, Padding = D4(padding), Origin = origin, Children = { new UI { Height = imgw, Width = imgw, Origin = Origin.MiddleLeft, Color = backgroundColorValue, Padding = D4(8), Children = { new UI { ID = id + "img", ImageColor = Color("0.9 0.9 0.9 1"), Image = itemid != 0 ? itemid.ToString() : category.Image(), ImageType = itemid == 0 ? ImageType.Simple : ImageType.Item } } }, new UI { ID = id + "bar", Origin = Origin.RowMiddle, Height = barHeight, Trim = D4(left: imgw + Padding), Position = P4(down: bardown), Color = XPBackgroundColor, Children = { new UI { ID = id + "bar.progress", Origin = D4(0, 0, Math.Max(0.05f, stats.XpProgress), 1), Trim = D4(1), Color = XPColor } } }, new UI { ID = id + "lvl", Origin = Origin.RowMiddle, Height = 24, Trim = D4(left: imgw + Padding), Position = P4(down: bardown, up: txtup, right: 1), Text = stats.Level.ToString(), TextSize = fontSize, TextAlign = UnityEngine.TextAnchor.LowerLeft }, new UI { ID = id + "text", Origin = Origin.RowMiddle, Height = 24, Trim = D4(left: imgw + Padding), Position = P4(down: bardown, up: txtup + (fontSize + 2), right: 1), Text = name, TextSize = fontSize, TextAlign = UnityEngine.TextAnchor.LowerLeft }, } }; } private static void ShowCategoryLevel(BasePlayer basePlayer, bool textOnly = false, bool animate = true, bool update = false) { var category = SelectedCategory.GetValueOrDefault(basePlayer.userID, CraftingCategoryType.Melee); var ui = SCOPE.Player(basePlayer); var skills = basePlayer.Skills(); if (update) { ui.Update(LevelID, (element) => { element.UpdateComponent((component) => { var category = SelectedCategory.GetValueOrDefault(basePlayer.userID, CraftingCategoryType.Melee); component.FadeIn = 0f; component.Color = Tracking.IsTracking(basePlayer.userID, category) ? PanelHighlightColor : PanelColor; return component; }); return element; }); ui.Update(LevelElementID + "img", (element) => { element.UpdateComponent((component) => { var category = SelectedCategory.GetValueOrDefault(basePlayer.userID, CraftingCategoryType.Melee); component.FadeIn = animate ? FadeIn : 0f; component.ImageKey = category.Image(); return component; }); return element; }); ui.Update(LevelElementID + "bar.progress", (element) => { element.UpdateComponent((component) => { var category = SelectedCategory.GetValueOrDefault(basePlayer.userID, CraftingCategoryType.Melee); component.Anchors = D4(0, 0, Math.Max(0.05f, skills[category].XpProgress), 1); return component; }); element.UpdateComponent((component) => { component.FadeIn = animate ? FadeIn : 0f; return component; }); return element; }); ui.Update(LevelElementID + "lvl", (element) => { element.UpdateComponent((component) => { var category = SelectedCategory.GetValueOrDefault(basePlayer.userID, CraftingCategoryType.Melee); component.FadeIn = animate ? FadeIn : 0f; component.Text = skills[category].Level.ToString(); return component; }); return element; }); ui.Update(LevelElementID + "text", (element) => { element.UpdateComponent((component) => { var category = SelectedCategory.GetValueOrDefault(basePlayer.userID, CraftingCategoryType.Melee); component.FadeIn = animate ? FadeIn : 0f; component.Text = category.DisplayName(basePlayer); return component; }); return element; }); return; } if (!textOnly) { var b = new UI { Parent = ContentID, ID = LevelID, FadeIn = animate ? FadeIn : 0, Origin = Origin.UpperRight, Height = CategoryLevelHeight, Width = RightWidth, Color = Tracking.IsTracking(basePlayer.userID, category) ? PanelHighlightColor : PanelColor, Material = PanelMaterial, Button = ClickAction((args) => { var category = SelectedCategory.GetValueOrDefault(basePlayer.userID, CraftingCategoryType.Melee); Tracking.Toggle(basePlayer.userID, category); Tracking.Show(basePlayer); PlaySFX(basePlayer, FX.ItemSelectFX); ShowCategoryLevel(basePlayer, update: true); ShowTracking(basePlayer, update: true); }) }; ui.Show(b); } var c = new UI { Parent = LevelID, ID = LevelContentID, FadeIn = textOnly ? 0 : animate ? FadeIn : 0, Children = { LevelElement(basePlayer, 0, category.DisplayName(basePlayer), category, skills[category], iconW: 64, padding: 8, fontSize: 14, barHeight: 12, id: LevelElementID) } }; ui.Show(c); } private static void ShowContentList(BasePlayer basePlayer, bool animate = true) { var page = SelectedPage.GetValueOrNew(basePlayer.userID); var category = SelectedCategory.GetValueOrDefault(basePlayer.userID, CraftingCategoryType.Melee); var count = 6; var skills = basePlayer.Skills(); var items = skills.ItemsForCategory(category); var remainder = 0; page.MaxPage = (items.Count / (float)count).Floor(); if (!page.EnableNext) { remainder = count - (items.Count % count); } var ui = SCOPE.Player(basePlayer); var t = new UI { ID = ListID, Parent = ContentID, //FadeIn = animate ? 1f : 0, //FadeIn = 1f, Trim = D4(left: LeftWidth + Gap, right: RightWidth + Gap, top: ContentHeaderHeight + ListGap, bottom: PaginationHeight + Gap), Children = { items.Count <= 0 ? null : new UI { ID = $"{ListID}.layout", //FadeIn = 1f, Layout = new UiLayout { Rows = count }, Children = UI.ForEach(items.OrderByDescending(x => x.Value.Level), (itemSkill) => { var itemDef = ItemManager.FindItemDefinition(itemSkill.Key); var chances = skills.GetQualityChances(itemDef.shortname); var name = $"{ID}.{itemDef.shortname}"; return new UI { ID = $"{ListID}.skill.{itemSkill.Key}", FadeIn = animate ? FadeIn : 0f, Trim = D4(ListGap), Color = Tracking.IsTracking(basePlayer.userID, itemDef.shortname) ? PanelHighlightColor : PanelColor, Material = PanelMaterial, //FadeIn = 1f, Button = ClickAction((args) => { Tracking.Toggle(basePlayer.userID, args[0]); Tracking.Show(basePlayer); PlaySFX(basePlayer, FX.ItemSelectFX); ShowTracking(basePlayer, update: true); ui.Update($"{ListID}.skill.{itemSkill.Key}", (element) => { element.UpdateComponent((component) => { component.Color = Tracking.IsTracking(basePlayer.userID, itemDef.shortname) ? PanelHighlightColor : PanelColor; return component; }); return element; }); }, itemDef.shortname, name), Children = { LevelElement(basePlayer, itemDef.itemid, itemDef.displayName.translated, CraftingCategoryType.None, itemSkill.Value, 48, Padding, 14, 14, origin: D4(0, 0, 0.5f, 1)), new UI { ID = $"{ListID}.skill.{itemSkill.Key}.qualities", Origin = D4(0.5f, 0, 1, 1), Layout = new UiLayout { Cols = 5 }, Children = UI.For(5, (i) => { var prob = chances[i] / 100f; //var prob = chances[i+1]; var isLessThanOne = prob < 0.01f && prob > 0f; var percent = prob.Percent(); return new UI { ID = $"{ListID}.skill.{itemSkill.Key}.qualities.{i}.text", Text = isLessThanOne ? "<1%" : percent, TextAlign = UnityEngine.TextAnchor.MiddleCenter, TextColor = prob <= 0f ? Color(1,1,1,0.1f) : QualityItem.GetQualityColor(i+1).Saturate(0.1f) }; }) } } }; }).Extend(UI.For(1, (i) => { var rh = 1f/count; var h = remainder * rh; var gaps = ListGap * (remainder); return new UI { ID = "blank", Trim = D4(top: ListGap), Origin = D4(left: 0, bottom: -(remainder-1), right: 1, top: 1), Color = PanelColor, Material = PanelMaterial }; })) }, items.Count > 0 ? null : new UI { Color = PanelColor, Text = basePlayer.Lang("menu no items crafted"), TextAlign = UnityEngine.TextAnchor.MiddleCenter, TextColor = FontColor } } }; ui.Show(t); } private static void ShowContentPaginator(BasePlayer basePlayer) { var ui = SCOPE.Player(basePlayer); var page = SelectedPage.GetValueOrNew(basePlayer.userID); var format = "{0}/{1}"; var c = new UI { Parent = ListPagID, ID = ListPagContentID, Padding = D4(Padding), Children = { new UI { Width = PagIconSize, Height = PagIconSize, Origin = Origin.MiddleLeft, Button = ClickPage($"{ListID}.layout", -1, $"{ListPagID}.txt", format, FX.LootCopyFX), ButtonImage = SPRITE.Enter, ButtonImageColor = FontColor }, new UI { Width = PagIconSize, Height = PagIconSize, Origin = Origin.MiddleRight, Button = ClickPage($"{ListID}.layout", 1, $"{ListPagID}.txt", format, FX.LootCopyFX), ButtonImage = SPRITE.Exit, ButtonImageColor = FontColor }, new UI { ID = $"{ListPagID}.txt", Origin = Origin.ColCenter, Width = 100, Text = string.Format(format, page.Page+1, page.MaxPage+1), TextAlign = UnityEngine.TextAnchor.MiddleCenter, TextColor = FontColor } } }; ui.Show(c); } private static void ShowContent(BasePlayer basePlayer) { var ui = SCOPE.Player(basePlayer); var t = new UI { Parent = ContentID, FadeIn = FadeIn, ID = ListTitleID, Origin = Origin.RowUpper, Trim = D4(left: LeftWidth + Gap, right: RightWidth + Gap), GrowY = Growth.Negative, Height = ContentHeaderHeight, Color = PanelColor, Material = PanelMaterial, Children = { new UI { Origin = D4(0, 0, 0.5f, 1), TextAlign = UnityEngine.TextAnchor.MiddleCenter, Text = basePlayer.Lang("menu item"), TextColor = FontColor }, new UI { Origin = D4(0.5f, 0, 1, 1), Layout = new UiLayout { Cols = 5 }, Children = UI.For(5, (i) => { return new UI { Height = StartIconSize, Width = StartIconSize, Origin = Origin.MiddleCenter, Image = $"star{i+1}", ImageType = ImageType.Simple, ImageColor = Color("1 1 0 1") }; }) } } }; ui.Show(t); var p = new UI { Parent = ContentID, ID = ListPagID, FadeIn = FadeIn, Origin = Origin.RowLower, Trim = D4(left: LeftWidth + Gap, right: RightWidth + Gap), Height = PaginationHeight, Color = PanelColor, Material = PanelMaterial }; ui.Show(p); } private static void ShowCategories(BasePlayer basePlayer, bool background = true) { var selectedCategory = SelectedCategory.GetValueOrDefault(basePlayer.userID, CraftingCategoryType.Melee); var ui = SCOPE.Player(basePlayer); if (background) { var b = new UI { Parent = ContentID, ID = CategoryID, FadeIn = FadeIn, Origin = Origin.ColLeft, Width = LeftWidth, Color = PanelColor, Material = PanelMaterial }; ui.Show(b); } var l = new UI { Parent = CategoryID, ID = CategoryLayoutID, FadeIn = FadeIn, Layout = new UiLayout { //ID = $"{CategoryLayoutButtonID}", Rows = 12 }, Children = UI.ForEach(CraftingCategory.All(), (categoryType) => { var size = 3; var selected = categoryType == selectedCategory; return new UI { ID = $"{CategoryLayoutButtonID}.{categoryType}", InheritFade = false, Color = selected ? HighlightColor : PanelColor, Trim = selected ? D4(-size) : D4(0), Button = ClickAction((args) => { var oldCat = SelectedCategory.GetValueOrDefault(basePlayer.userID, CraftingCategoryType.Melee); var newCat = (CraftingCategoryType)int.Parse(args[0]); SelectedCategory[basePlayer.userID] = newCat; PlaySFX(basePlayer, FX.LootCopyFX); ui.Update($"{CategoryLayoutButtonID}.{oldCat}", (element) => { element.UpdateComponent((component) => { component.Color = PanelColor; return component; }); element.UpdateComponent((component) => { component.Offsets = component.Offsets.Merge(D4(size)); return component; }); return element; }); ui.Update($"{CategoryLayoutButtonID}.{newCat}", (element) => { element.UpdateComponent((component) => { component.Color = HighlightColor; return component; }); element.UpdateComponent((component) => { component.Offsets = component.Offsets.Merge(D4(-size)); return component; }); return element; }); ShowCategoryLevel(basePlayer, update: true); ShowContentList(basePlayer, animate: true); ShowPerks(basePlayer, update: true); ShowLeaderboard(basePlayer, textOnly: true); ShowContentPaginator(basePlayer); }, (int)categoryType), Children = { new UI { InheritFade = false, Width = CategoryIconSize, Height = CategoryIconSize, Position = P4(right: Padding), Origin = Origin.MiddleLeft, Image = categoryType.Image(), ImageType = ImageType.Simple, ImageColor = FontColor }, new UI { InheritFade = false, Trim = P4(left: Padding + CategoryIconSize + Padding, right: Padding), Text = categoryType.DisplayName(basePlayer), TextAlign = UnityEngine.TextAnchor.MiddleLeft, TextColor = FontColor } } }; }) }; ui.Show(l); } private static void ShowTracking(BasePlayer basePlayer, bool textOnly = false, bool update = false) { var ui = SCOPE.Player(basePlayer); var tracking = Tracking.GetTrackingDetails(basePlayer.userID); if (tracking.Count <= 0) { ui.Destroy(TrackingID); return; } if (update && tracking.Count > 1) { ui.Update(TrackingTextID, (element) => { element.UpdateComponent((component) => { component.Text = basePlayer.Lang("menu tracking") + $" {tracking.Count}/{Tracking.MaxTracked}"; return component; }); return element; }); return; } var c = new UI { Parent = ListPagID, ID = TrackingID, Width = 300, Height = 32, Origin = Origin.LowerCenter, GrowY = Growth.Negative, Position = P4(down: Gap), Color = PanelColor, Material = PanelMaterial, Children = { new UI { ID = TrackingTextID, TextColor = FontColor, TextAlign = UnityEngine.TextAnchor.MiddleCenter, Text = basePlayer.Lang("menu tracking") + $" {tracking.Count}/{Tracking.MaxTracked}" } } }; ui.Show(c); } public static void ReloadCurrentContent(BasePlayer basePlayer) { ShowCategoryLevel(basePlayer, update: true, animate: false); ShowContentList(basePlayer, animate: false); ShowPerks(basePlayer, update: true); //ShowLeaderboard(basePlayer, true); } public static void Close(BasePlayer basePlayer) { SelectedCategory.Remove(basePlayer.userID); SelectedPage.Remove(basePlayer.userID); PlayerOpenedMenu.Remove(basePlayer.userID); SCOPE?.Player(basePlayer).Destroy(ID); Interface.CallHook("QC_SkillMenuClosed", basePlayer); } public static void Show(BasePlayer basePlayer) { var ui = SCOPE.Player(basePlayer); PlayerOpenedMenu.Add(basePlayer.userID); var b = new UI { Parent = "Overlay", ID = ID, Color = Color("0 0 0 0.95"), Material = MATERIAL.UIBackgroundBlurInGameMenu, NeedsCursor = true, Children = { new UI { Origin = Origin.MiddleCenter, Height = Height, Width = Width, Position = P4(up: 40), FadeIn = FadeIn, Children = { new UI { Origin = Origin.RowUpper, GrowY = Growth.Negative, Height = TitleHeight, Color = PanelColor, Material = PanelMaterial, Children = { new UI { Origin = D4(0.3f, 0, 0.7f, 1), Text = basePlayer.Lang("menu crafting skills"), TextColor = FontColor, TextAlign = UnityEngine.TextAnchor.MiddleCenter }, new UI { ID = $"{ID}.close", Height = CloseBtnSize, Width = CloseBtnSize, Position = P4(left: Padding), Origin = Origin.MiddleRight, Button = ClickAction((args) => { PlaySFX(basePlayer, FX.LootCopyFX); Close(basePlayer); }), ButtonImage = SPRITE.Close, ButtonImageColor = FontColor } } }, new UI { ID = ContentID, Trim = D4(top: TitleHeight + Gap) } } }, new UI { Origin = Origin.LowerCenter, Width = 700, Height = 30, TextAlign = UnityEngine.TextAnchor.LowerCenter, TextSize = 16, //GrowY = Growth.Negative, Position = P4(left: 30, up: 16), Text = $"{basePlayer.Lang("tip").ToUpper()}: {basePlayer.Lang(Const.Tips.Random())}", } } }; ui.Show(b); //b.Show(basePlayer); ShowCategories(basePlayer); ShowCategoryLevel(basePlayer); ShowPerks(basePlayer); ShowLeaderboard(basePlayer); ShowContent(basePlayer); ShowContentList(basePlayer); ShowContentPaginator(basePlayer); ShowTracking(basePlayer); Interface.CallHook("QC_SkillMenuOpened", basePlayer); } } } } namespace Oxide.Plugins { public partial class QualityCrafting { public static class Notifications { private const string ID = "qc.notification"; private const float FadeOut = 0.6f; private const float FadeIn = 0.1f; private const float Duration = 1.6f; private const float Width = 48f; private const float Height = 48f; private const float ImageInset = -8; private const float ImagePos = 60; private static UI QualityItemOverlay(QualityItem qi, BasePlayer basePlayer, string id) { if (qi.Quality <= 0) { return UI.Empty; } float size = 0.5f * CONFIG.InventoryOverlay.QualityStarsSizeScale; return new UI { ID = $"{id}.qio", Origin = D4(1f - size, 1f - size, 1, 1), InheritFade = true, Image = $"star{qi.Quality}", ImageColor = Const.StarColor }; } public static void Show(BasePlayer basePlayer, QualityItem qi, bool duplicated = false) { if (!CONFIG.Notifications.Enabled) { return; } if (CONFIG.Notifications.PlaySfx) { PlaySFX(basePlayer, FX.ResearchSuccess); if (duplicated) { PlaySFX(basePlayer, FX.ItemUnlock); } } var config = CONFIG.Notifications; var b = new UI { ID = ID, Parent = "Hud", Origin = Origin.LowerCenter, Width = Width, Height = Height, Inset = D4(-18), Position = P4(left: config.X, up: config.Y), Duration = Duration, FadeIn = FadeIn, FadeOut = FadeOut, Children = { new UI { Image = qi.Item.info.itemid, ImageType = ImageType.Item, Inset = D4(ImageInset), Height = Height, Width = Height, Origin = Origin.MiddleCenter, //Position = D4(right: ImagePos+Offset), Children = { QualityItemOverlay(qi, basePlayer, ID + "img") } }, new UI { Image = SPRITE.SquareGradient, ImageColor = Color(0, 0, 0, 0.6f), Origin = Origin.MiddleCenter, Position = P4(down: 16), Width = Width*2, Height = 24, Children = { new UI { Text = basePlayer.Lang("notification crafted").ToUpper(), TextSize = 18, TextColor = Color(1, 1, 1, 1f), TextAlign = TextAnchor.MiddleCenter, GrowY = Growth.Positive } } }, !duplicated ? null : new UI { Text = basePlayer.Lang("notification duplicated").ToUpper(), TextAlign = TextAnchor.MiddleCenter, TextSize = 11, Position = P4(down: 34f), TextColor = Color(0.9f, 0.6f, 0.9f) }, new UI { InheritFade = false, Image = SPRITE.SquareGradient, Inset = D4(-8), ImageColor = qi.QualityColor.Set(a: 0.5f), Duration = 0f, FadeOut = 0.8f, } } }; SCOPE.Player(basePlayer).Show(b); } } } } namespace Oxide.Plugins { public partial class QualityCrafting { public struct RollResults { public float Result { get; set; } public bool MeetsOrBeats(float chance) => Result <= chance; public bool IsBetween(float min, float max) => Result >= min && Result <= max; } public static RollResults Roll() { return new RollResults { Result = UnityEngine.Random.Range(0f, 100f) }; } } } namespace Oxide.Plugins { public partial class QualityCrafting { public class PlayerCategoryStatistics { public int NumQualityItemsCrafted = 0; public int NumItemsCrafted = 0; public int HighestQualityCrafted = 0; public int ItemsDuplicated = 0; public float TotalCraftTime = 0f; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public int? ClothUsed = null; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public int? SpringsUsed = null; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public int? RopeUsed = null; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public int? MetalBladesUsed = null; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public int? ResourcesGained = null; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public int? ItemsRefined = null; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public float? DamageDealt = null; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public float? FarthestHit = null; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public float? DamageTaken = null; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public float? Headshots = null; [JsonIgnore] public float AvgCraftTime => NumItemsCrafted == 0 ? 0 : (float)Math.Round(TotalCraftTime / (float)NumItemsCrafted, 1); [JsonIgnore] public float PercentQualityItems => (float) Math.Round(NumItemsCrafted == 0f ? 0f : (NumQualityItemsCrafted / (float)NumItemsCrafted) * 100f, 2); public int LeaderboardRank(ulong userId, CraftingCategoryType category) => Leaderboards.CategoryRankings[category].FindIndex(x => x.UserID == userId)+1; } public class PlayerStatistics { public PlayerStatistics() { foreach(var category in CraftingCategory.All()) { Categories[category] = new PlayerCategoryStatistics(); } } public Dictionary Categories { get; set; } = new Dictionary(); } public static class Statistics { private static Dictionary Data = new Dictionary(); public static PlayerStatistics Get(ulong userId) { if (!Data.ContainsKey(userId)) { Data[userId] = new PlayerStatistics(); } return Data[userId]; } public static void LoadStatistics(string prefix = "v3") { var data = Interface.Oxide.DataFileSystem.ReadObject>($"{INSTANCE.Name}/{prefix}/PlayerStatistics") ?? new Dictionary(); Data = data; } public static void SaveStatistics(string prefix = "v3") { Interface.Oxide.DataFileSystem.WriteObject($"{INSTANCE.Name}/{prefix}/PlayerStatistics", Data); } public static void Clear() { Data = new Dictionary(); } } } } namespace Oxide.Plugins { public partial class QualityCrafting { public static class Tables { // ItemLevels public const string DEFAULT_ITEM_LEVELS_TABLE = "ItemLevels"; private static CsvNumberTable _itemLevelsTable = null; public static CsvNumberTable ItemLevelsTable { get { _itemLevelsTable ??= CSV.LoadCsv(CONFIG.ItemLeveling.Table); return _itemLevelsTable; } } // CategoryLevels public const string DEFAULT_CATEGORY_LEVELS_TABLE = "CategoryLevels"; private static CsvNumberTable _categoryLevelsTable = null; public static CsvNumberTable CategoryLevelsTable { get { _categoryLevelsTable ??= CSV.LoadCsv(CONFIG.CategoryLeveling.Table); return _categoryLevelsTable; } } // QualityChances public const string DEFAULT_QUALITY_CHANCES_TABLE = "QualityChances"; private static CsvNumberTable _qualityChancesTable = null; public static CsvNumberTable QualityChancesTable { get { _qualityChancesTable ??= CSV.LoadCsv(CONFIG.QualityTiers.Table); return _qualityChancesTable; } } public static void CreateDefaultItemLevelsTable(bool forced) { if (!forced && CSV.Exists(DEFAULT_ITEM_LEVELS_TABLE)) { return; } var rows = new List(); for (int lvl = 0; lvl < 100; lvl++) { var value = (float)Math.Round((lvl / (float)5) + 2); // formula rows.Add(new float[] { value }); } var data = string.Join('\n', rows.Select(x => string.Join(',', x))); CSV.SaveCsv(DEFAULT_ITEM_LEVELS_TABLE, data); } public static void CreateDefaultCategoryLevelsTable(bool forced) { if (!forced && CSV.Exists(DEFAULT_CATEGORY_LEVELS_TABLE)) { return; } var rows = new List(); for (int lvl = 0; lvl < 100; lvl++) { var value = (float) Math.Round((lvl / (float)5) + 5); // formula rows.Add(new float[] { value }); } var data = string.Join('\n', rows.Select(x => string.Join(',', x))); CSV.SaveCsv(DEFAULT_CATEGORY_LEVELS_TABLE, data); } private static readonly List Fibonacchi = new List() { /* 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 */ 1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181 }; private static float GetValueAt(float x, float x1, float y1, float x2, float y2) { var m = (y2 - y1) / (x2 - x1); var b = y1 - m * x1; float y = m * x + b; return y.Round(); } public static void CreateDefaultQualityChancesTable(bool forced) { // x = level // y = quality if (!forced && CSV.Exists(DEFAULT_QUALITY_CHANCES_TABLE)) { return; } var steps = new List>(); for (int q = 0; q < 5; q++) { steps.Add(new List()); for(int stp = 0; stp <= 10; stp++) { if (stp == 0) { steps[q].Add(0); continue; } if (q == 0 && stp == 1) { steps[q].Add(Fibonacchi[4]); } else if (q == 1 && stp == 2) { steps[q].Add(Fibonacchi[3]); } else if (q == 2 && stp == 3) { steps[q].Add(Fibonacchi[2]); } else if (q == 3 && stp == 4) { steps[q].Add(Fibonacchi[1]); } else if (q == 4 && stp == 5) { steps[q].Add(Fibonacchi[0]); } else { // get fib index of previous step for quality var prev_value = steps[q].Last(); var fib_index = prev_value == 0 ? -1 : Fibonacchi.FindIndex(x => x == (int)prev_value.Floor()); if (fib_index == -1) { steps[q].Add(0); } else { steps[q].Add(Fibonacchi[fib_index + 1]); } } } } var rows = new List(); for (int lvl = 0; lvl < 100; lvl++) { var cols = new float[5]; for (int q = 0; q < 5; q++) { if (lvl % 10 == 0) { var stpIdx = (lvl / 10.0).Floor(); cols[q] = steps[q][stpIdx]; } else { var stpIdx1 = (lvl / 10.0).Floor(); var stpIdx2 = stpIdx1 + 1; var x1 = (stpIdx1 * 10.0).Floor(); var x2 = (stpIdx2 * 10.0).Floor(); var y1 = steps[q][stpIdx1]; var y2 = steps[q][stpIdx2]; var value = GetValueAt(lvl, x1, y1, x2, y2); cols[q] = value; } } rows.Add(cols); } var data = string.Join('\n', rows.Select(x => string.Join(',', x))); CSV.SaveCsv(DEFAULT_QUALITY_CHANCES_TABLE, data); } } } } namespace Oxide.Plugins { public partial class QualityCrafting : CovalencePlugin { public class TrackingDetail { [JsonIgnore] public string ID => $"{(ItemShortName == null ? Category.ToString() : ItemShortName.ToString())}"; public string ItemShortName { get; set; } = null; public CraftingCategoryType Category { get; set; } = CraftingCategoryType.None; [JsonIgnore] public ItemDefinition ItemDefinition => ItemManager.FindItemDefinition(ItemShortName); public string DisplayName(BasePlayer basePlayer) => ItemShortName == null ? Category.DisplayName(basePlayer) : ItemDefinition.displayName.translated; [JsonIgnore] public string Image => ItemShortName == null ? Category.Image() : string.Empty; [JsonIgnore] public bool IsItem => ItemShortName != null; } public static class Tracking { private const string ID = "qc.tracking"; private const int Width = 120; private const int TrackHeight = 30; public static int MaxTracked => CONFIG.Tracking.MaxTrackedItems; private const int Padding = 2; private static readonly UiColor PanelColor = Color("0.7 0.7 0.65 0.05"); //private static readonly string PanelMaterial = BetterCUI.Material.UIBackgroundBlur; private static readonly string PanelMaterial = ""; public static readonly UiColor XPBarBackgroundColor = COLOR.Rust.DarkGray; public static readonly UiColor XPBarBackgroundHighlightColor = COLOR.Rust.LightGreen; public static Dictionary> PlayerTracking = new Dictionary>(); public static List DefualtTrackingList() { var def = new List(); def.AddRange(CONFIG.Categories.Where(x => x.Value.TrackedByDefault).Select(x => new TrackingDetail { Category = Enum.Parse(x.Key) })); return def; } public static void Toggle(ulong userId, string itemId) { if (IsTracking(userId, itemId)) { Untrack(userId, itemId); } else { Track(userId, itemId); } } public static void Toggle(ulong userId, CraftingCategoryType category) { if (IsTracking(userId, category)) { Untrack(userId, category); } else { Track(userId, category); } } public static void Track(ulong userId, CraftingCategoryType category) { var tracking = PlayerTracking.GetValueOrNew(userId, DefualtTrackingList()); if (tracking.Count >= MaxTracked) { return; } tracking.AddIfNotExists((x) => x.Category == category, new TrackingDetail { Category = category }); } public static void Track(ulong userId, string itemId) { var tracking = PlayerTracking.GetValueOrNew(userId, DefualtTrackingList()); if (tracking.Count >= MaxTracked) { return; } tracking.AddIfNotExists((x) => x.ItemShortName == itemId, new TrackingDetail { ItemShortName = itemId }); } public static TrackingDetail Get(ulong userId, string itemId) => PlayerTracking.GetValueOrDefault(userId)?.FirstOrDefault(x => x.ItemShortName == itemId); public static TrackingDetail Get(ulong userId, CraftingCategoryType category) => PlayerTracking.GetValueOrDefault(userId)?.FirstOrDefault(x => x.Category == category); public static List GetTrackingDetails(ulong userId) => PlayerTracking.GetValueOrNew(userId, DefualtTrackingList()); public static void Untrack(ulong userId, string itemId) => PlayerTracking.GetValueOrDefault(userId)?.RemoveAll(x => x.ItemShortName == itemId); public static void Untrack(ulong userId, CraftingCategoryType category) => PlayerTracking.GetValueOrDefault(userId)?.RemoveAll(x => x.Category == category); public static bool IsTracking(ulong userId, string itemId) => PlayerTracking.GetValueOrDefault(userId)?.Any(x => x.ItemShortName == itemId) ?? false; public static bool IsTracking(ulong userId, CraftingCategoryType category) => PlayerTracking.GetValueOrDefault(userId)?.Any(x => x.Category == category) ?? false; private static string LevelElementID(TrackingDetail tracking) => $"{ID}.{tracking.ID}.levelelement"; private static UI LevelElement(BasePlayer basePlayer, TrackingDetail tracking, bool levelup = false, int change = 0) { var imgw = TrackHeight - 4 - Padding * 2; var skills = basePlayer.Skills(); var stats = tracking.IsItem ? skills.Items.GetValueOrDefault(tracking.ItemShortName) : skills.Categories[tracking.Category]; if (stats == null) { return null; } return new UI { ID = LevelElementID(tracking), Parent = $"{ID}.{tracking.ID}", Padding = D4(4), Color = Color("0.2 0.2 0.2 0.7"), Children = { new UI { ID = LevelElementID(tracking) + "sprite", Height = imgw-4, Width = imgw-4, Origin = Origin.MiddleLeft, ImageColor = Color("1 1 1 1"), Image = tracking.IsItem ? tracking.ItemDefinition.itemid.ToString() : tracking.Image, ImageType = tracking.IsItem ? ImageType.Item : ImageType.Simple }, new UI { ID = LevelElementID(tracking) + "bar", Origin = Origin.RowLower, Height = 6, Trim = D4(left: imgw + 4), Color = XPBarBackgroundColor, Children = { new UI { ID = LevelElementID(tracking) + "bar.progress", Origin = D4(0, 0, Math.Max(0.05f, stats.XpProgress), 0), Height = 6, GrowY = Growth.Positive, Trim = D4(1), Color = XPBarBackgroundHighlightColor, Children = { new UI { Duration = 0f, FadeOut = 0.5f, Color = Color("1 1 1 1") } } } } }, new UI { ID = LevelElementID(tracking) + "lvl", Origin = Origin.RowLower, Height = 24, Trim = D4(left: imgw + 4), Position = P4(up: 7, right: 1), Text = stats.Level.ToString(), TextSize = 8, TextAlign = UnityEngine.TextAnchor.LowerLeft }, stats.Level >= 100 ? null : new UI { Enabled = change != 0, Duration = 0.5f, FadeOut = 0.1f, Origin = Origin.RowLower, Height = 24, Trim = D4(left: imgw + 4), Position = P4(up: 8, left: 1), Text = $"+{change} {basePlayer.Lang("xp")}", TextSize = 6, TextAlign = UnityEngine.TextAnchor.LowerRight }, stats.Level >= 100 ? null : new UI { Enabled = levelup, ID = $"{ID}.{tracking.ID}.effect", Trim = D4(-6), Color = Color("1 1 1 1"), Duration = 0f, FadeOut = 0.5f } } }; } public static void Refresh(BasePlayer basePlayer, TrackingDetail tracking, bool levelup, int change) { if (tracking == null) { return; } var ui = SCOPE.Player(basePlayer); var skills = basePlayer.Skills(); var stats = tracking.IsItem ? skills.Items[tracking.ItemShortName] : skills.Categories[tracking.Category]; if (levelup) { PlaySFX(basePlayer, FX.LockCodeUpdated); } else { PlaySFX(basePlayer, FX.LootCopyFX); } ui.Update(LevelElementID(tracking) + "bar.progress", (element) => { element.UpdateComponent((component) => { component.Anchors = D4(0, 0, Math.Max(0.05f, stats.XpProgress), 0); return component; }); return element; }); ui.Update(LevelElementID(tracking) + "lvl", (element) => { element.UpdateComponent((component) => { component.Text = stats.Level.ToString(); return component; }); return element; }); var xpgainEffect = levelup || stats.Level >= 100 ? null : new UI { Parent = LevelElementID(tracking) + "bar.progress", Duration = 0f, FadeOut = 0.5f, Color = Color("1 1 1 1") }; var xpgainTxt = change == 0 || (stats.Level >= 100 && !levelup) ? null : new UI { Parent = LevelElementID(tracking), Duration = 0.5f, FadeOut = 0.1f, Origin = Origin.RowLower, Height = 24, Position = P4(up: 8, left: 1), Text = $"+{change} {basePlayer.Lang("xp")}", TextSize = 6, TextAlign = UnityEngine.TextAnchor.LowerRight }; var lvlupEffect = !levelup ? null : new UI { Parent = LevelElementID(tracking), Trim = D4(-4), Color = Color("1 1 1 1"), Duration = 0f, FadeOut = 0.5f }; ui.Show(xpgainEffect, xpgainTxt, lvlupEffect); } public static void Destroy(BasePlayer basePlayer) { SCOPE?.Player(basePlayer).Destroy(ID); } public static void Show(BasePlayer basePlayer, bool initial = false) { var details = GetTrackingDetails(basePlayer.userID) .Where(x => x.Category.IsEnabled() || x.IsItem) .ToList(); if (initial || details.Count > MaxTracked) { var skills = basePlayer.Skills(); details = details.Where(x => !x.IsItem || skills.Items.ContainsKey(x.ItemShortName)).Take(MaxTracked).ToList(); PlayerTracking[basePlayer.userID] = details; } if (details.Count <= 0) { Destroy(basePlayer); return; } var ui = SCOPE.Player(basePlayer); var c = new UI { Parent = "Under", ID = ID, Origin = Origin.LowerCenter, Position = P4(up: CONFIG.Tracking.Y, right: CONFIG.Tracking.X), Width = Width, Height = TrackHeight * (MaxTracked + 1), Layout = new UiLayout { Rows = MaxTracked + 1, ReverseRows = true, }, Children = UI.ForEach(details, (e) => { return new UI { ID = $"{ID}.{e.ID}", Trim = D4(2), Color = PanelColor, Children = { LevelElement(basePlayer, e) } }; }).Extend(new UI { Enabled = details.Count > 0, Trim = D4(bottom: 2), Text = basePlayer.Lang("tracking crafting skills").ToUpper(), TextAlign = UnityEngine.TextAnchor.LowerCenter, TextSize = 10, TextColor = Color("1 1 1 0.5") }) }; ui.Destroy(ID); ui.Show(c); } public static void LoadPlayerTracking() { var data = Interface.Oxide.DataFileSystem.ReadObject>>($"{INSTANCE.Name}/v3/PlayerTracking") ?? new Dictionary>(); PlayerTracking = data; } public static void SavePlayerTracking() { Interface.Oxide.DataFileSystem.WriteObject($"{INSTANCE.Name}/v3/PlayerTracking", PlayerTracking); } } } } namespace Oxide.Plugins { public partial class QualityCrafting { public enum CraftingCategoryType { None, Melee, Tools, Bows, Firearms, Clothing } public readonly Dictionary DefaultCategoryNames = new Dictionary { [CraftingCategoryType.Melee] = "Melee", [CraftingCategoryType.Tools] = "Tools", [CraftingCategoryType.Bows] = "Bows", [CraftingCategoryType.Firearms] = "Firearms", [CraftingCategoryType.Clothing] = "Clothing", //[CraftingCategoryType.Healing] = "Healing", }; public static class CraftingCategory { private static CraftingCategoryType[] _allCategories = null; public static CraftingCategoryType[] All() { if (_allCategories == null) { _allCategories = Enum.GetValues(typeof(CraftingCategoryType)).Cast() .Where(x => x != CraftingCategoryType.None && CONFIG.Categories[x.ToString()].Enabled) .ToArray(); } return _allCategories; } public static CraftingCategoryType GetCraftingCategoryType(ItemDefinition itemDef) { if (itemDef == null) { return CraftingCategoryType.None; } try { var item = ItemManager.CreateByItemID(itemDef.itemid); if (item == null) { return CraftingCategoryType.None; } return GetCraftingCategoryType(item); } catch (Exception e) { return CraftingCategoryType.None; } } public static CraftingCategoryType GetCraftingCategoryType(Item item) { var info = item.info; var category = item.info.category; if (category == ItemCategory.Weapon) { var heldEntity = item.GetHeldEntity(); if (heldEntity is ProjectileWeaponMod) { // No mods } else if (heldEntity is CompoundBowWeapon || heldEntity is CrossbowWeapon || heldEntity is BowWeapon) { return CraftingCategoryType.Bows; } else if (heldEntity is BaseProjectile) { return CraftingCategoryType.Firearms; } else if (heldEntity is BaseMelee) { return CraftingCategoryType.Melee; } } else if (category == ItemCategory.Tool && item.hasCondition) { return CraftingCategoryType.Tools; } else if (category == ItemCategory.Attire && !item.IsBackpack()) { return CraftingCategoryType.Clothing; } // TODO healing items return CraftingCategoryType.None; } } } } namespace Oxide.Plugins { public partial class QualityCrafting { public class QualityChance { public QualityChance(float chance, float probability) { Chance = chance; Probability = probability; } public float Chance { get; set; } public float Probability { get; set; } public int Percent => Chance < 0.01 ? 0 : (int)Math.Floor(Chance * 100); } public class LevelXP { public int XP { get; set; } = 0; public int Level { get; set; } = 0; public bool IsItemXp { get; set; } = true; public LevelXP() { } public LevelXP(bool isItem) { IsItemXp = isItem; } [JsonIgnore] public float XpProgress { get { float val = 0f; if (Level >= 100) { return 1f; } if (IsItemXp) { val =Math.Min(1f, XP / (float)CraftingSkills.XpRequiredForItemLevel[Level]); } else { val = Math.Min(1f, XP / (float)CraftingSkills.XpRequiredForCategoryLevel[Level]); } return float.IsNaN(val) ? 0f : val; } } public void GrantXP(int xp, Action callback = null) { if (Level >= 100) { Level = 100; callback?.Invoke(false); return; } var XpRequirements = IsItemXp ? CraftingSkills.XpRequiredForItemLevel : CraftingSkills.XpRequiredForCategoryLevel; XP += xp; var required = XpRequirements[Level]; var levelup = false; while(XP >= XpRequirements[Level]) { var leftover = XP - required; Level++; required = XpRequirements[Level]; XP = leftover; levelup = true; } callback?.Invoke(levelup); } public void SetLevel(int level) { Level = Math.Max(0, Math.Min(100, level)); XP = 0; } } public class CraftingSkills { public LevelXP this[CraftingCategoryType key] { get { return Categories.GetValueOrDefault(key); } set { Categories[key] = value; } } public LevelXP this[string key] { get { return Items.GetValueOrNew(key); } set { Items[key] = value; } } public static Dictionary XpRequiredForCategoryLevel = new Dictionary(); public static Dictionary XpRequiredForItemLevel = new Dictionary(); public Dictionary Categories = new Dictionary() { [CraftingCategoryType.Melee] = new LevelXP(false), [CraftingCategoryType.Tools] = new LevelXP(false), [CraftingCategoryType.Firearms] = new LevelXP(false), [CraftingCategoryType.Bows] = new LevelXP(false), [CraftingCategoryType.Clothing] = new LevelXP(false), //[CraftingCategoryType.Healing] = new LevelXP(false) }; public Dictionary Items { get; private set; } = new Dictionary(); public Dictionary ItemsForCategory(CraftingCategoryType category) { return Items.Where(x => CraftingCategory.GetCraftingCategoryType(ItemManager.FindItemDefinition(x.Key)) == category).ToDictionary(x => x.Key, x => x.Value); } public float GetCraftingSpeed(CraftingCategoryType category) { if (category == CraftingCategoryType.None) { return 1f; } var speedPerLevel = CONFIG.CategoryLeveling.CraftingSpeedIncreasePerLevel; return 1f + (this[category].Level * speedPerLevel); } public float GetDuplicateChance(CraftingCategoryType category) => CONFIG.CategoryLeveling.DuplicationChancePerLevel * Categories[category].Level; public int GetRefiningQuality(CraftingCategoryType category) { if (category == CraftingCategoryType.None) { return 1; } var catLvl = Categories[category].Level; if (catLvl >= 80) { return 5; } if (catLvl >= 60) { return 4; } if (catLvl >= 40) { return 3; } if (catLvl >= 20) { return 2; } return 1; } private static Dictionary _cachedQualityChances = new Dictionary(); public float[] GetQualityChances(string itemShortName) { var itemLvl = this[itemShortName].Level; if (_cachedQualityChances.ContainsKey(itemLvl)) { return _cachedQualityChances[itemLvl]; } var table = Tables.QualityChancesTable; var chances = itemLvl == 0 ? new float[] { 0, 0, 0, 0, 0 } : new float[] { table.Get(itemLvl, 1), table.Get(itemLvl, 2), table.Get(itemLvl, 3), table.Get(itemLvl, 4), table.Get(itemLvl, 5), }; var accum = 0f; for (int i = 4; i >= 0; i--) { var chance = chances[i]; var rem = 100f - accum; chances[i] = chance < 1f ? 0 : Math.Max(0, Math.Min(rem, chance)); accum += chance; } _cachedQualityChances[itemLvl] = chances; return chances; } public int GetPerkCount(string itemId) { var chances = GetQualityChances(itemId); int count = 5; var roll = Roll(); var total = 0f; for (int i = 0; i < 5; i++) { var chance = chances[4 - i]; if (roll.IsBetween(total, total + chance)) { return 5 - i; } total += chance; } return 0; } public static void LoadXpRequirements() { for (int i = 0; i <= 100; i++) { XpRequiredForCategoryLevel[i] = i == 0 ? 0 : (int) Tables.CategoryLevelsTable.Get(i, 1); } for (int i = 0; i <= 100; i++) { XpRequiredForItemLevel[i] = i == 0 ? 0 : (int)Tables.ItemLevelsTable.Get(i, 1); } } } } } namespace Oxide.Plugins { public partial class QualityCrafting { public class ItemPerk { public string PerkID { get; set; } public int Rank { get; set; } public PerkDefinition Perk => Perks.ALL.GetValueOrDefault(PerkID, null); public float Bonus => Perk.Modifier / 100f * Rank; public float Multiplier => 1f + Bonus; public float Percentage => Perk.Modifier * Rank; public string RankText { get { if (Rank == 1) { return "I"; } if (Rank == 2) { return "II"; } if (Rank == 3) { return "III"; } if (Rank == 4) { return "IV"; } if (Rank >= 5) { return "V"; } return ""; } } public static List GetRandomItemPerks(Item item, CraftingCategoryType category, int maxRank = QualityItem.MaxQuality, ICollection exclude = null, QualityItem qi = null) { if (category == CraftingCategoryType.None) { var perks = new List() { new ItemPerk { PerkID = Perks.Lethal.ID, Rank = maxRank } }; return perks; } var validPerks = Perks.ALL.Where(x => (exclude == null || !exclude.Contains(x.Key)) && (!CONFIG.Options.PreventUselessPerks || (x.Value.IsPerkUseful?.Invoke(item) ?? true))).Select(x => x.Key).ToList(); if (validPerks.Count == 0) { return new List(); } var weightedPerksForCategory = WeightedPerksPerCategory[category].Where(x => validPerks.Contains(x)).ToArray().ToList(); if (weightedPerksForCategory.Count == 0) { return new List(); } var sum = PerksForCategory[category].Select(x => Perks.ALL[x]).Sum(x => x.MaxRank); var given = maxRank; maxRank = Math.Min(maxRank, sum); var selectedPerks = new Dictionary(); int totalRank = 0; int tries = 0; if (qi == null) { qi = item.Quality(); } while (totalRank < maxRank) { if (weightedPerksForCategory.Count == 0) { break; } if (tries > 5) { INSTANCE.PrintWarning("Max tries exceeded"); break; } var randomPerkId = weightedPerksForCategory.Random(); var currentRankForPerk = selectedPerks.GetValueOrDefault(randomPerkId) + qi.GetPerkRank(randomPerkId); var randomPerk = Perks.ALL[randomPerkId]; if (currentRankForPerk < randomPerk.MaxRank) { selectedPerks[randomPerkId] = selectedPerks.GetValueOrDefault(randomPerkId) + 1; totalRank++; } else { weightedPerksForCategory.RemoveAll(x => x == randomPerkId); } tries++; } return selectedPerks.Select(x => new ItemPerk { PerkID = x.Key, Rank = x.Value }).ToList(); } } } } namespace Oxide.Plugins { public partial class QualityCrafting { public class PerkDefinition : PerkConfig { public string ID { get; set; } public int Priority { get; set; } = 0; public bool IsNegative { get; set; } = false; public string DefaultName { get; set; } = ""; public string DefaultDescription { get; set; } = ""; public string PluginName { get; set; } = "QualityCrafting"; public List Categories { get; set; } = new List(); public Action OnDamageDealt; public Action OnDamageTaken; public Action OnItemCreated; public Action OnItemLoaded; public Action OnItemUnloaded; public Action OnClothingEquipped; public Action OnClothingUnequipped; public Action OnWeaponFired; public Action OnWeaponThrown; public Action OnResourceHit; public Action OnPlayerTick; public Action OnItemSwapped; public Action OnItemUnswapped; public Action OnEntityKilled; public Func IsPerkUseful; public string NameLocalized(BasePlayer basePlayer) => basePlayer.Lang($"{ID} name"); public string DescriptionLocalized(BasePlayer basePlayer) => basePlayer.Lang($"{ID} description"); public string IconID => $"{ID}.icon"; } } } namespace Oxide.Plugins { public partial class QualityCrafting : CovalencePlugin { public class ClothingResistances { public Dictionary Head = new Dictionary(); public Dictionary Body = new Dictionary(); public Dictionary Legs = new Dictionary(); public HashSet TypesWithBonuses = new HashSet(); } public class ClothingProtectionStat { public Rust.DamageType DamageType { get; set; } public string Sprite { get; set; } public string TextId { get; set; } } public partial class QualityItem { public Dictionary ProtectionBonus = new Dictionary(); public float GetBonusResistance(Rust.DamageType damageType, HitArea? area = null) { return !Item.info.isWearable ? 0f : (area == null || (Item.info?.ItemModWearable?.armorProperties?.Contains(area.Value) ?? true)) ? ProtectionBonus?.GetValueOrDefault(damageType, 0f) ?? 0f : 0f; } public float GetBaseResistance(Rust.DamageType damageType, HitArea? area = null) { return !Item.info.isWearable ? 0f : (area == null || Item.info.ItemModWearable.armorProperties.Contains(area.Value)) ? Item.info.ItemModWearable.protectionProperties.TryGet(damageType) : 0f; } public float GetTotalProtection(Rust.DamageType damageType, HitArea? area = null) => GetBaseResistance((Rust.DamageType)damageType, area) + GetBonusResistance(damageType, area); public static List GetClothingProtectionStats() { return new List { new ClothingProtectionStat { DamageType = Rust.DamageType.Bullet, Sprite = SPRITE.Bullet, TextId = "inspection stat bullet" }, new ClothingProtectionStat { DamageType = Rust.DamageType.Stab, Sprite = SPRITE.Stab, TextId = "inspection stat stab" }, new ClothingProtectionStat { DamageType = Rust.DamageType.Explosion, Sprite = SPRITE.Explosion, TextId = "inspection stat explosion" }, new ClothingProtectionStat { DamageType = Rust.DamageType.ColdExposure, Sprite = SPRITE.Freezing, TextId = "inspection stat freezing" }, new ClothingProtectionStat { DamageType = Rust.DamageType.RadiationExposure, Sprite = SPRITE.Radiation, TextId = "inspection stat radiation" }, new ClothingProtectionStat { DamageType = Rust.DamageType.Bite, Sprite = SPRITE.Bite, TextId = "inspection stat bite" }, new ClothingProtectionStat { DamageType = Rust.DamageType.Slash, Sprite = SPRITE.Slash, TextId = "inspection stat slash" }, new ClothingProtectionStat { DamageType = Rust.DamageType.Fall, Sprite = SPRITE.Fall, TextId = "inspection stat fall" }, new ClothingProtectionStat { DamageType = Rust.DamageType.Blunt, Sprite = SPRITE.Blunt, TextId = "inspection stat blunt" }, new ClothingProtectionStat { DamageType = Rust.DamageType.ElectricShock, Sprite = SPRITE.Electric, TextId = "inspection stat electric" } }; } } } } namespace Oxide.Plugins { public partial class QualityCrafting { public partial class QualityItem { public const int MaxQuality = 5; private int? _quality { get; set; } = null; private BaseEntity _heldEntity = null; public int Quality { get { if (!_quality.HasValue) { _quality = Math.Min(MaxQuality, Perks.Sum(x => x.Rank)); } return _quality.Value; } } public int Tier => Item?.info.Blueprint?.workbenchLevelRequired ?? 0; public Item Item { get; private set; } = null; public ulong CreatorUserId { get; private set; } = 0; private string _customDisplayName = null; private string _customDescription = null; public string DisplayName => string.IsNullOrWhiteSpace(_customDisplayName) ? Item.info.displayName.translated : _customDisplayName; public string Description => string.IsNullOrWhiteSpace(_customDescription) ? Item.info.displayDescription.translated : _customDescription; private readonly List _perks = new List(); private ItemPerk[] _perksInOrder = null; public ItemPerk[] Perks { get { _perksInOrder ??= _perks.OrderByDescending(x => x.Perk.Priority).ToArray(); return _perksInOrder; } } public string _legendaryID = null; public bool IsLegendary => !string.IsNullOrWhiteSpace(_legendaryID); public bool AnyPerks => Perks.Any(); public bool HasPerksApplied { get; private set; } = false; public string ShortName => Item.info.shortname; public UiColor QualityColor => GetQualityColor(Quality); public CraftingCategoryType Category => CraftingCategory.GetCraftingCategoryType(Item); public QualityItem Duplicate() { var newItem = ItemManager.CreateByItemID(Item.info.itemid); return Duplicate(newItem); } public QualityItem Duplicate(Item newItem) { if (newItem.uid == Item.uid) { return this; } var qi = QualityItem.Parse(newItem, true); qi.SetPerks(_perks); return qi; } public void SetLegendary(string id) { _legendaryID = id; SetQualityItemData(); } public bool HasPerk(string perkId) { return Perks.Any(x => x.PerkID == perkId); } public T GetHeldEntity() where T : class { if (_heldEntity == null) { _heldEntity = Item.GetHeldEntity(); } return _heldEntity as T; } public void SetQuality(int quality) { var perks = ItemPerk.GetRandomItemPerks(Item, Category, quality, qi: this); SetPerks(perks); } public int GetPerkRank(string perkId) { return Perks.FirstOrDefault(x => x.PerkID == perkId)?.Rank ?? 0; } public void AddPerks(IEnumerable itemPerks) { foreach (var itemPerk in itemPerks) { var ip = _perks.FirstOrDefault(x => x.PerkID == itemPerk.PerkID); if (ip == null) { _perks.Add(itemPerk); } else { ip.Rank = Math.Min(ip.Perk.MaxRank, ip.Rank + itemPerk.Rank); }; } SetQualityItemData(); } public void SetPerks(IEnumerable itemPerks) { foreach (var itemPerk in itemPerks) { SetPerk(itemPerk.PerkID, itemPerk.Rank); } } public void SetPerk(string perkId, int rank) { var perk = QualityCrafting.Perks.ALL.GetValueOrDefault(perkId); if (perk == null) { return; } SetPerk(perk, rank); } public void SetPerk(PerkDefinition perk, int rank) { var ip = _perks.FirstOrDefault(x => x.PerkID == perk.ID); _perks.RemoveAll(x => x.PerkID == perk.ID); if (rank > 0) { _perks.Add(new ItemPerk { PerkID = perk.ID, Rank = rank }); } else { ip?.Perk.OnItemUnloaded?.Invoke(ip, this); } SetQualityItemData(); } public void Rename(string name, string description) { Item.name = name; if (!string.IsNullOrWhiteSpace(name)) { _customDisplayName = name.Replace("|", ""); } if (!string.IsNullOrWhiteSpace(description)) { _customDescription = description.Replace("|", ""); } SetQualityItemData(); } public void ClearPerks() { _perks.Clear(); SetQualityItemData(); } public void AddPerk(string perkId, int rank) { var perk = QualityCrafting.Perks.ALL.GetValueOrDefault(perkId); if (perk == null) { return; } AddPerk(perk, rank); } public void AddPerk(PerkDefinition perk, int rank) { var ip = _perks.FirstOrDefault(x => x.PerkID == perk.ID); if (ip == null) { _perks.Add(new ItemPerk { PerkID = perk.ID, Rank = rank }); } else { ip.Rank = Math.Min(perk.MaxRank, ip.Rank + rank); } SetQualityItemData(); } public void RemovePerk(PerkDefinition perk, int rank = 0) => RemovePerk(perk.ID, rank); public void RemovePerk(string perkId, int rank = 0) { var ip = _perks.FirstOrDefault(x => x.PerkID == perkId); if (ip == null) { return; } if (rank <= 0) { _perks.RemoveAll(x => x.PerkID == perkId); } else { ip.Rank -= rank; if (ip.Rank <= 0) { _perks.RemoveAll(x => x.PerkID == perkId); } } ip.Perk.OnItemUnloaded?.Invoke(ip, this); SetQualityItemData(); } public static QualityItem ParseLegacy(Item item) { try { string text = item.text; int quality = 0; ulong creatorId = 0; if (string.IsNullOrEmpty(text)) { quality = 0; } else { quality = int.Parse(text.Substring(0, 1)); if (text.Length > 1) { ulong uid = 0; if (ulong.TryParse(text.Substring(1, text.Length - 1), out uid)) { creatorId = uid; } } } var qi = new QualityItem(); qi.Item = item; qi.CreatorUserId = creatorId; qi.SetQuality(quality); return qi; } catch (Exception) { INSTANCE.PrintWarning($"Failed to parse quality item text data. Text was '{item.text}'"); item.text = string.Empty; return null; } } public static QualityItem Parse(Item item, bool forced = false) { var qiExisting = CachedQualityItems.GetValueOrDefault(item.uid.Value); if (qiExisting != null) { return qiExisting; } //var category = CraftingCategory.GetCraftingCategoryType(item); //if (!item.IsQualityItem() /*&& category == CraftingCategoryType.None */ && !forced) { return null; } try { var qi = new QualityItem(); qi.Item = item; var text = item.text; if (string.IsNullOrWhiteSpace(text)) { return qi; } // text = '02343454054|perk1.3,perk2.1,perk3.1|0|custoname|customdescription|legendaryid' var authorThenPerksThenApplied = text.Split("|"); // Author var authorUserId = ulong.Parse(authorThenPerksThenApplied[0]); qi.CreatorUserId = authorUserId; // Perks var perks = authorThenPerksThenApplied[1]; if (!string.IsNullOrWhiteSpace(perks)) { var perksWithRank = perks.Split(","); foreach (var perkWithRank in perksWithRank) { var split = perkWithRank.Split("."); var perk = split[0]; var rank = int.Parse(split[1]); if(!QualityCrafting.Perks.ALL.ContainsKey(perk)) { continue; } qi._perks.Add(new ItemPerk { PerkID = perk, Rank = rank }); } } // Applied var applied = int.Parse(authorThenPerksThenApplied[2]); qi.HasPerksApplied = applied != 0; // CustomName qi._customDisplayName = authorThenPerksThenApplied.Length > 3 ? authorThenPerksThenApplied[3] : null; // CustomDescription qi._customDescription = authorThenPerksThenApplied.Length > 4 ? authorThenPerksThenApplied[4] : null; // LegendaryID qi._legendaryID = authorThenPerksThenApplied.Length > 5 ? authorThenPerksThenApplied[5] : null; if (!string.IsNullOrWhiteSpace(qi._legendaryID)) { // Load legendary } // Call loaded hook qi.ProtectionBonus.Clear(); foreach (var itemPerk in qi.Perks) { if (itemPerk.Perk.OnItemLoaded!= null) { itemPerk.Perk.OnItemLoaded(itemPerk, qi); } } CachedQualityItems[qi.Item.uid.Value] = qi; return qi; } catch (Exception e) { return ParseLegacy(item); } } private void SetQualityItemData() { var data = $"{CreatorUserId}|{(string.Join(",", _perks.Select(x => $"{x.PerkID}.{x.Rank}")))}|{(HasPerksApplied ? 1 : 0)}|{_customDisplayName ?? string.Empty}|{_customDescription ?? string.Empty}|{_legendaryID ?? string.Empty}"; Item.text = data; _perksInOrder = null; foreach (var itemPerk in Perks) { itemPerk.Perk.OnItemLoaded?.Invoke(itemPerk, this); } HasPerksApplied = true; _quality = null; CachedQualityItems.Remove(Item.uid.Value); } public static UiColor GetQualityColor(int quality) { if (quality == 1) { return Color(0.4f, 0.929f, 0.361f, 1f); } if (quality == 2) { return Color(0.2f, 0.459f, 0.91f, 1f); } if (quality == 3) { return Color(0.69f, 0.38f, 0.839f, 1f); } if (quality == 4) { return Color(1f, 0.839f, 0f, 1f); } if (quality == 5) { return Color(1f, 0.412f, 0f, 1f); } return Color(1,1,1,1); } public XpAward XPAward() { return new XpAward { CategoryXp = CONFIG.CategoryLeveling.XpGainedForItemWorkbenchTier[Tier], ItemXp = CONFIG.ItemLeveling.XpGainedForItemWorkbenchTier[Tier] }; } } } } namespace Oxide.Plugins { public partial class QualityCrafting { public partial class QualityItem : CovalencePlugin { public float GatherScaleBonus { get; set; } = 0f; public float GatherScale => 1f + GatherScaleBonus; private float? _baseFleshGather = null; private float? _baseOreGather = null; private float? _baseTreeGather = null; public float BaseGather(ResourceDispenser.GatherType type) { if (_baseFleshGather == null || _baseOreGather == null || _baseTreeGather == null) { var fakeItem = ItemManager.Create(Item.info); var heldEntity = fakeItem.GetHeldEntity() as AttackEntity; if (heldEntity is BaseMelee casted) { _baseFleshGather = casted.gathering.Flesh.gatherDamage; _baseOreGather = casted.gathering.Ore.gatherDamage; _baseTreeGather = casted.gathering.Tree.gatherDamage; } else { _baseFleshGather = 0f; _baseOreGather = 0f; _baseTreeGather = 0f; } fakeItem.Remove(); } if (type == ResourceDispenser.GatherType.Flesh) { return _baseFleshGather.Value; } if (type == ResourceDispenser.GatherType.Ore) { return _baseOreGather.Value; } if (type == ResourceDispenser.GatherType.Tree) { return _baseTreeGather.Value; } return 0f; } public float TotalGather(ResourceDispenser.GatherType type) => BaseGather(type) * GatherScale; public float BonusGather(ResourceDispenser.GatherType type) => TotalGather(type) - BaseGather(type); } } } namespace Oxide.Plugins { public partial class QualityCrafting { public partial class QualityItem : CovalencePlugin { public float DamageScaleBonus { get; set; } = 0f; public float DamageScale => 1f + DamageScaleBonus; public float TemporaryScaleBonus { get; set; } = 0f; private float? _baseDamage = null; public float BaseDamage { get { if (_baseDamage == null) { var heldEntity = GetHeldEntity(); if (heldEntity is BaseMelee casted) { _baseDamage = casted.TotalDamage(); } else { _baseDamage = 0f; } } return _baseDamage.Value; } } public float TotalDamage => BaseDamage * DamageScale; public float BonusDamage => TotalDamage - BaseDamage; } } }