diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_IvtrEquip.cs b/Assets/PerfectWorld/Scripts/Managers/EC_IvtrEquip.cs new file mode 100644 index 0000000000..05415cde77 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Managers/EC_IvtrEquip.cs @@ -0,0 +1,1653 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using ModelRenderer.Scripts.GameData; +using BrewMonster; +using BrewMonster.Common; +using BrewMonster.Network; +using System.IO; +using System.Text.RegularExpressions; +using System.Reflection; + +namespace PerfectWorld.Scripts.Managers +{ + /// + /// Equipment item class that handles all equipment-specific functionality + /// Converted from C++ EC_IvtrEquip.cpp + /// + public class EC_IvtrEquip + { + #region Constants and Enums + + // Item Class IDs + public const int ICID_EQUIP = 1; + public const int ICID_WEAPON = 2; + + // Item Made From Types + public const byte IMT_NULL = 0; + public const byte IMT_SIGN = 1; + + // Property Effect Essence Flags + public const uint PEE_PHYDAMAGE = 0x00000001; + public const uint PEE_MAGICDAMAGE = 0x00000002; + public const uint PEE_PHYDEF = 0x00000004; + public const uint PEE_GOLDDEF = 0x00000008; + public const uint PEE_WOODDEF = 0x00000010; + public const uint PEE_WATERDEF = 0x00000020; + public const uint PEE_FIREDEF = 0x00000040; + public const uint PEE_EARTHDEF = 0x00000080; + public const uint PEE_ATKSPEED = 0x00000100; + public const uint PEE_ATKDIST = 0x00000200; + public const uint PEE_HP = 0x00000400; + public const uint PEE_MP = 0x00000800; + public const uint PEE_DODGE = 0x00001000; + public const uint PEE_ENDURANCE = 0x00002000; + public const uint PEE_STRENGTHREQ = 0x00004000; + public const uint PEE_AGILITYREQ = 0x00008000; + public const uint PEE_ENERGYREQ = 0x00010000; + public const uint PEE_VITALITYREQ = 0x00020000; + + // Property Effect Essence Indexes + public const int PEEI_PHYDAMAGE = 0; + public const int PEEI_MAX_PHYDAMAGE = 1; + public const int PEEI_MAGICDAMAGE = 2; + public const int PEEI_MAX_MAGICDAMAGE = 3; + public const int PEEI_PHYDEF = 4; + public const int PEEI_GOLDDEF = 5; + public const int PEEI_WOODDEF = 6; + public const int PEEI_WATERDEF = 7; + public const int PEEI_FIREDEF = 8; + public const int PEEI_EARTHDEF = 9; + public const int PEEI_ATKDIST = 10; + public const int PEEI_HP = 11; + public const int PEEI_MP = 12; + public const int PEEI_DODGE = 13; + public const int MAX_PEEINDEX = 14; + + // Refine Indexes + public const int REFINE_PHYDAMAGE = 0; + public const int REFINE_MAGICDAMAGE = 1; + public const int REFINE_PHYDEF = 2; + public const int REFINE_GOLDDEF = 3; + public const int REFINE_WOODDEF = 4; + public const int REFINE_WATERDEF = 5; + public const int REFINE_FIREDEF = 6; + public const int REFINE_EARTHDEF = 7; + public const int REFINE_HP = 8; + public const int REFINE_DODGE = 9; + public const int MAX_REFINEINDEX = 10; + + // Item Description Colors + public const int ITEMDESC_COL_WHITE = 0; + public const int ITEMDESC_COL_GREEN = 1; + public const int ITEMDESC_COL_YELLOW = 2; + public const int ITEMDESC_COL_DARKGOLD = 3; + public const int ITEMDESC_COL_LIGHTBLUE = 4; + public const int ITEMDESC_COL_CYANINE = 5; + public const int ITEMDESC_COL_RED = 6; + public const int ITEMDESC_COL_GRAY = 7; + + // Item Description String IDs + public const int ITEMDESC_NAME = 1000; + public const int ITEMDESC_ADDPHYDAMAGE = 1001; + public const int ITEMDESC_MAXPHYDAMAGE = 1002; + public const int ITEMDESC_ADDMAGICDAMAGE = 1003; + public const int ITEMDESC_MAXMAGICDAMAGE = 1004; + public const int ITEMDESC_PHYDEFENCE = 1005; + public const int ITEMDESC_ALLMAGICDEF = 1006; + public const int ITEMDESC_GOLDDEFENCE = 1007; + public const int ITEMDESC_WOODDEFENCE = 1008; + public const int ITEMDESC_WATERDEFENCE = 1009; + public const int ITEMDESC_FIREDEFENCE = 1010; + public const int ITEMDESC_EARTHDEFENCE = 1011; + public const int ITEMDESC_ADDHP = 1012; + public const int ITEMDESC_ADDMP = 1013; + public const int ITEMDESC_STRENGTH = 1014; + public const int ITEMDESC_AGILITY = 1015; + public const int ITEMDESC_ENERGY = 1016; + public const int ITEMDESC_VITALITY = 1017; + public const int ITEMDESC_DODGE = 1018; + public const int ITEMDESC_DEADLYSTRIKE = 1019; + public const int ITEMDESC_ATKRATING = 1020; + public const int ITEMDESC_ATKTIME = 1021; + public const int ITEMDESC_ADDATKDIST = 1022; + public const int ITEMDESC_CASTTIME = 1023; + public const int ITEMDESC_ENDURANCE = 1024; + public const int ITEMDESC_REPAIRCOST = 1025; + public const int ITEMDESC_EQUIPMARK = 1026; + public const int ITEMDESC_MADEFROM = 1027; + public const int ITEMDESC_2STRINGS = 1028; + public const int ITEMDESC_REPUTATION_REQ = 1029; + public const int ITEMDESC_EQUIP_DESTROYING = 1030; + public const int ITEMDESC_EQUIP_REPAIR_NEED_ITEM = 1031; + public const int ITEMDESC_EQUIP_REPAIR_NEED_ITEMCNT = 1032; + public const int ITEMDESC_ERRORPROP = 1033; + public const int ITEMDESC_RANDOMPROP = 1034; + public const int ITEMDESC_ATK_DEGREE = 1035; + public const int ITEMDESC_DEF_DEGREE = 1036; + public const int ITEMDESC_SOULPOWER = 1037; + public const int ITEMDESC_VIGOUR = 1038; + public const int ITEMDESC_ADDRIDEONPETSPEED = 1039; + public const int ITEMDESC_PENETRATION = 1040; + public const int ITEMDESC_RESILIENCE = 1041; + public const int ITEMDESC_PROFVIEW = 1042; + public const int ITEMDESC_REQEXTRA = 1043; + + // Scale Types + public const int SCALE_SELL = 1; + + // Endurance Scale + public const int ENDURANCE_SCALE = 100; + + #endregion + + #region Fields + + // Basic Item Properties + public int TemplateId { get; set; } + public int ExpireDate { get; set; } + public int CID { get; set; } + public int Price { get; set; } + public int Count { get; set; } + public float PriceScale { get; set; } + public int ScaleType { get; set; } + + // Equipment Requirements + public int LevelReq { get; set; } + public int StrengthReq { get; set; } + public int AgilityReq { get; set; } + public int ProfReq { get; set; } + public int VitalityReq { get; set; } + public int EnergyReq { get; set; } + public int ReputationReq { get; set; } + + // Endurance + public int CurEndurance { get; set; } + public int MaxEndurance { get; set; } + public int RepairFee { get; set; } + + // Maker Information + public byte MadeFrom { get; set; } + public string Maker { get; set; } + + // Equipment Properties + public ushort StoneMask { get; set; } + public int FixProps { get; set; } + public int RefineLvl { get; set; } + public byte PropNum { get; set; } + public byte EmbedNum { get; set; } + + // Equipment Arrays + public List Holes { get; set; } + public List Props { get; set; } + + // Description + protected string m_strDesc = ""; + + #endregion + + #region Base Stats (from Element Data) + + private static bool TryGetNumber(object data, string[] names, out T value) where T : struct + { + value = default; + if (data == null) return false; + var t = data.GetType(); + foreach (var name in names) + { + var f = t.GetField(name, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase); + if (f != null) + { + try + { + object v = f.GetValue(data); + if (v == null) continue; + if (typeof(T) == typeof(float)) + { + float fv = Convert.ToSingle(v); + value = (T)(object)fv; + return true; + } + else + { + int iv = Convert.ToInt32(v); + if (typeof(T) == typeof(int)) { value = (T)(object)iv; return true; } + } + } + catch { } + } + var p = t.GetProperty(name, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase); + if (p != null) + { + try + { + object v = p.GetValue(data, null); + if (v == null) continue; + if (typeof(T) == typeof(float)) + { + float fv = Convert.ToSingle(v); + value = (T)(object)fv; + return true; + } + else + { + int iv = Convert.ToInt32(v); + if (typeof(T) == typeof(int)) { value = (T)(object)iv; return true; } + } + } + catch { } + } + } + return false; + } + + public string GetBaseStatsForDisplay() + { + return GetBaseStatsDesc(); + } + + private string GetBaseStatsDesc() + { + elementdataman edm = ElementDataManProvider.GetElementDataMan(); + if (edm == null) return string.Empty; + + try + { + DATA_TYPE dt = DATA_TYPE.DT_INVALID; + object data = edm.get_data_ptr(unchecked((uint)TemplateId), ID_SPACE.ID_SPACE_ESSENCE, ref dt); + if (data == null) data = TryFindElementByScanningArraysLocal(edm, unchecked((uint)TemplateId)); + if (data == null) return string.Empty; + + // Try weapon-style fields first + int dmgLow = 0, dmgHighMax = 0; + bool hasDmgLow = TryGetNumber(data, new[] { "damage_low" }, out dmgLow); + bool hasDmgHighMax = TryGetNumber(data, new[] { "damage_high_max", "damage_high" }, out dmgHighMax); + + List lines = new List(); + + if (hasDmgLow && hasDmgHighMax) + { + // Physical damage range + lines.Add(string.Format("{0} {1}-{2}", GetItemDescString(ITEMDESC_ADDPHYDAMAGE), dmgLow, dmgHighMax)); + + // Magic damage if available + if (TryGetNumber(data, new[] { "magic_damage_low" }, out int mdLow) && + TryGetNumber(data, new[] { "magic_damage_high_max", "magic_damage_high" }, out int mdHigh)) + { + lines.Add(string.Format("{0} {1}-{2}", GetItemDescString(ITEMDESC_ADDMAGICDAMAGE), mdLow, mdHigh)); + } + + // Attack time/speed + float atkTime; + if (TryGetNumber(data, new[] { "attack_time", "atk_time" }, out atkTime) && atkTime > 0.0001f) + { + // Show speed in attacks per second (1 / time) + float aps = 1.0f / atkTime; + lines.Add(string.Format("{0} {1:F2}", GetItemDescString(ITEMDESC_ATKTIME), aps)); + } + else if (TryGetNumber(data, new[] { "attack_speed", "atk_speed" }, out float atkSpeed) && atkSpeed > 0) + { + lines.Add(string.Format("{0} {1:F2}", GetItemDescString(ITEMDESC_ATKTIME), atkSpeed)); + } + + // Range + float range; + if (TryGetNumber(data, new[] { "range", "attack_range", "atk_range" }, out range)) + { + lines.Add(string.Format("{0} {1:F2}", GetItemDescString(ITEMDESC_ADDATKDIST), range)); + } + } + else + { + // Try armor-style fields + if (TryGetNumber(data, new[] { "defence_high", "defense_high", "defence", "defense" }, out int defHigh)) + { + lines.Add(string.Format("{0} +{1}", GetItemDescString(ITEMDESC_PHYDEFENCE), defHigh)); + } + + // Elemental resistances (look for *_resist or specific names) + (string label, string[] names)[] res = new (string, string[])[] + { + (GetItemDescString(ITEMDESC_GOLDDEFENCE), new[]{"resist_metal","resistance_metal","gold_resist"}), + (GetItemDescString(ITEMDESC_WOODDEFENCE), new[]{"resist_wood","resistance_wood","wood_resist"}), + (GetItemDescString(ITEMDESC_WATERDEFENCE), new[]{"resist_water","resistance_water","water_resist"}), + (GetItemDescString(ITEMDESC_FIREDEFENCE), new[]{"resist_fire","resistance_fire","fire_resist"}), + (GetItemDescString(ITEMDESC_EARTHDEFENCE), new[]{"resist_earth","resistance_earth","earth_resist"}) + }; + for (int i = 0; i < res.Length; i++) + { + var label = res[i].label; + var names = res[i].names; + if (TryGetNumber(data, names, out int v)) + { + lines.Add(string.Format("{0} +{1}", label, v)); + } + } + } + + return string.Join("\\r", lines); + } + catch { return string.Empty; } + } + + private static object TryFindElementByScanningArraysLocal(object edm, uint id) + { + if (edm == null) return null; + try + { + var fields = edm.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance); + foreach (var f in fields) + { + if (!f.FieldType.IsArray) continue; + var arr = f.GetValue(edm) as Array; + if (arr == null || arr.Length == 0) continue; + var elemType = f.FieldType.GetElementType(); + var idField = elemType.GetField("id", BindingFlags.Public | BindingFlags.Instance); + if (idField == null || idField.FieldType != typeof(uint)) continue; + for (int i = 0; i < arr.Length; i++) + { + var element = arr.GetValue(i); + if (element == null) continue; + var value = (uint)idField.GetValue(element); + if (value == id) + { + return element; + } + } + } + } + catch { } + return null; + } + + #endregion + + #region Property Structure + + public class Property + { + public int Type { get; set; } + public int NumParam { get; set; } + public bool Embed { get; set; } + public bool Suite { get; set; } + public bool Engraved { get; set; } + public bool Local { get; set; } + public int[] Params { get; set; } + + public Property() + { + Params = new int[4]; + } + + public Property(Property other) + { + Type = other.Type; + NumParam = other.NumParam; + Embed = other.Embed; + Suite = other.Suite; + Engraved = other.Engraved; + Local = other.Local; + Params = new int[4]; + Array.Copy(other.Params, Params, Math.Min(4, other.Params.Length)); + } + } + + #endregion + + #region Constructor + + public EC_IvtrEquip(int tid, int expireDate) + { + TemplateId = tid; + ExpireDate = expireDate; + CID = ICID_EQUIP; + Price = 0; + Count = 1; + PriceScale = 1.0f; + ScaleType = 0; + + LevelReq = 0; + StrengthReq = 0; + AgilityReq = 0; + ProfReq = 0; + VitalityReq = 0; + EnergyReq = 0; + ReputationReq = 0; + CurEndurance = 0; + MaxEndurance = 0; + RepairFee = 0; + MadeFrom = 0; + StoneMask = 0; + FixProps = 0; + RefineLvl = 0; + PropNum = 0; + EmbedNum = 0; + Holes = new List(); + Props = new List(); + } + + public EC_IvtrEquip(EC_IvtrEquip other) + { + // Copy basic properties + TemplateId = other.TemplateId; + ExpireDate = other.ExpireDate; + CID = other.CID; + Price = other.Price; + Count = other.Count; + PriceScale = other.PriceScale; + ScaleType = other.ScaleType; + + // Copy equipment properties + LevelReq = other.LevelReq; + ProfReq = other.ProfReq; + StrengthReq = other.StrengthReq; + AgilityReq = other.AgilityReq; + VitalityReq = other.VitalityReq; + EnergyReq = other.EnergyReq; + ReputationReq = other.ReputationReq; + CurEndurance = other.CurEndurance; + MaxEndurance = other.MaxEndurance; + RepairFee = other.RepairFee; + MadeFrom = other.MadeFrom; + Maker = other.Maker; + StoneMask = other.StoneMask; + FixProps = other.FixProps; + RefineLvl = other.RefineLvl; + PropNum = other.PropNum; + EmbedNum = other.EmbedNum; + + Holes = new List(other.Holes); + Props = new List(other.Props.Select(p => new Property(p))); + } + + #endregion + + #region Core Methods + + /// + /// Set item detail information from binary data + /// + public bool SetItemInfo(byte[] infoData, int dataLen) + { + if (infoData == null || dataLen == 0) + return true; + + // Try native order (as original client): + // [6 x short requirements][2 x int endurance][short essenceSize][maker info][essence bytes][short numHole][WORD stoneMask][numHole x int holes][int numProp][props] + if (TryParseEquipInfoNative(infoData, dataLen)) + { + ParseProperties(); + return true; + } + + // Fallback to legacy/custom order if server payload differs + if (TryParseEquipInfoLegacy(infoData, dataLen)) + { + ParseProperties(); + return true; + } + + Debug.LogError("EC_IvtrEquip::SetItemInfo: could not parse detail payload"); + return false; + } + + private bool TryParseEquipInfoNative(byte[] data, int len) + { + try + { + int offset = 0; + + if (len < 6 * 2 + 2 * 4 + 2) return false; + + LevelReq = BitConverter.ToInt16(data, offset); offset += 2; + ProfReq = BitConverter.ToInt16(data, offset); offset += 2; + StrengthReq= BitConverter.ToInt16(data, offset); offset += 2; + VitalityReq= BitConverter.ToInt16(data, offset); offset += 2; + AgilityReq = BitConverter.ToInt16(data, offset); offset += 2; + EnergyReq = BitConverter.ToInt16(data, offset); offset += 2; + + CurEndurance = BitConverter.ToInt32(data, offset); offset += 4; + MaxEndurance = BitConverter.ToInt32(data, offset); offset += 4; + + int essenceSize = BitConverter.ToInt16(data, offset); offset += 2; + + // Maker info (type + length + payload) + if (offset + 2 <= len) + { + ReadMakerInfo(data, ref offset); + } + + if (essenceSize < 0 || offset + essenceSize > len) return false; + offset += essenceSize; // skip essence for now + + if (offset + 2 + 2 > len) return false; + int numHole = BitConverter.ToInt16(data, offset); offset += 2; + StoneMask = BitConverter.ToUInt16(data, offset); offset += 2; + + Holes.Clear(); + if (numHole > 0) + { + if (offset + 4 * numHole > len) return false; + Holes.Capacity = numHole; + for (int i = 0; i < numHole; i++) + { + Holes.Add(BitConverter.ToInt32(data, offset)); offset += 4; + } + } + else if (numHole < 0) + { + return false; + } + + if (offset + 4 > len) return false; + int numProp = BitConverter.ToInt32(data, offset); offset += 4; + + Props.Clear(); + if (numProp > 0) + { + Props.Capacity = numProp; + for (int i = 0; i < numProp; i++) + { + if (offset + 4 > len) return false; + int type = BitConverter.ToInt32(data, offset); offset += 4; + + Property prop = new Property(); + prop.Type = type & 0x1fff; + prop.NumParam = (type & 0x6000) >> 13; + prop.Embed = (type & 0x8000) != 0; + prop.Suite = (type & 0x10000) != 0; + prop.Engraved = (type & 0x20000) != 0; + prop.Local = false; + + for (int j = 0; j < prop.NumParam; j++) + { + if (offset + 4 > len) return false; + prop.Params[j] = BitConverter.ToInt32(data, offset); offset += 4; + } + Props.Add(prop); + } + } + else if (numProp < 0) + { + return false; + } + + // Sanity check to catch misalignment + if (LevelReq < 0 || LevelReq > 2000) return false; + if (MaxEndurance < 0 || MaxEndurance > 1000000) return false; + + return true; + } + catch { return false; } + } + + private bool TryParseEquipInfoLegacy(byte[] data, int len) + { + try + { + int offset = 0; + if (len < 16 + 6 * 2 + 8 + 2) return false; + + // A legacy format we used earlier that prefixed price/scale before requirements + Price = BitConverter.ToInt32(data, offset); offset += 4; + Count = BitConverter.ToInt32(data, offset); offset += 4; + PriceScale = BitConverter.ToSingle(data, offset); offset += 4; + ScaleType = BitConverter.ToInt32(data, offset); offset += 4; + + LevelReq = BitConverter.ToInt16(data, offset); offset += 2; + ProfReq = BitConverter.ToInt16(data, offset); offset += 2; + StrengthReq= BitConverter.ToInt16(data, offset); offset += 2; + VitalityReq= BitConverter.ToInt16(data, offset); offset += 2; + AgilityReq = BitConverter.ToInt16(data, offset); offset += 2; + EnergyReq = BitConverter.ToInt16(data, offset); offset += 2; + + CurEndurance = BitConverter.ToInt32(data, offset); offset += 4; + MaxEndurance = BitConverter.ToInt32(data, offset); offset += 4; + + int essenceSize = BitConverter.ToInt16(data, offset); offset += 2; + ReadMakerInfo(data, ref offset); + if (essenceSize < 0 || offset + essenceSize > len) return false; + offset += essenceSize; + + int numHole = BitConverter.ToInt16(data, offset); offset += 2; + StoneMask = BitConverter.ToUInt16(data, offset); offset += 2; + Holes.Clear(); + if (numHole > 0) + { + if (offset + 4 * numHole > len) return false; + for (int i = 0; i < numHole; i++) { Holes.Add(BitConverter.ToInt32(data, offset)); offset += 4; } + } + else if (numHole < 0) return false; + + if (offset + 4 > len) return false; + int numProp = BitConverter.ToInt32(data, offset); offset += 4; + Props.Clear(); + if (numProp > 0) + { + for (int i = 0; i < numProp; i++) + { + if (offset + 4 > len) return false; + int type = BitConverter.ToInt32(data, offset); offset += 4; + Property prop = new Property(); + prop.Type = type & 0x1fff; + prop.NumParam = (type & 0x6000) >> 13; + prop.Embed = (type & 0x8000) != 0; + prop.Suite = (type & 0x10000) != 0; + prop.Engraved = (type & 0x20000) != 0; + for (int j = 0; j < prop.NumParam; j++) + { + if (offset + 4 > len) return false; + prop.Params[j] = BitConverter.ToInt32(data, offset); offset += 4; + } + Props.Add(prop); + } + } + + return true; + } + catch { return false; } + } + + /// + /// Read maker information from binary data + /// + private void ReadMakerInfo(byte[] data, ref int offset) + { + MadeFrom = data[offset++]; + int makerLen = data[offset++]; + + if (makerLen > 0) + { + if (MadeFrom == IMT_SIGN) + { + ushort color = BitConverter.ToUInt16(data, offset); offset += 2; + makerLen -= 2; + + string maker = System.Text.Encoding.Unicode.GetString(data, offset, makerLen); + offset += makerLen; + + if (string.IsNullOrEmpty(maker)) + { + Debug.LogWarning($"EC_IvtrEquip::ReadMakerInfo: Invalid maker info with makerLen={makerLen + 2}"); + return; + } + + SetNewMark(maker, ColorFromWord(color)); + } + else + { + Maker = System.Text.Encoding.Unicode.GetString(data, offset, makerLen); + offset += makerLen; + } + } + else + { + Maker = ""; + } + } + + /// + /// Set new mark with color + /// + public void SetNewMark(string mark, Color color) + { + Maker = mark; + if (!string.IsNullOrEmpty(Maker)) + { + // Convert hint string + Maker = EC_GameUIMan.AUI_ConvertHintString(Maker); + + // Filter bad words + // g_pGame->GetGameRun()->GetUIManager()->FilterBadWords(m_strMaker); + + // Add color + string colorStr = $"^FF{color.r:X2}{color.g:X2}{color.b:X2}"; + Maker = colorStr + Maker; + + // Add equipment mark display string + Maker = string.Format(GetItemDescString(ITEMDESC_EQUIPMARK), Maker); + } + MadeFrom = string.IsNullOrEmpty(mark) ? IMT_NULL : IMT_SIGN; + } + + #endregion + + #region Utility Methods + + /// + /// Get item name + /// + public string GetName() + { + return EC_IvtrItem.ResolveItemName(TemplateId); + } + + /// + /// Add current endurance + /// + public int AddCurEndurance(int value) + { + CurEndurance += value; + CurEndurance = Mathf.Clamp(CurEndurance, 0, MaxEndurance); + return CurEndurance; + } + + /// + /// Convert endurance real value to displaying value + /// + public static int VisualizeEndurance(int v) + { + return (v + ENDURANCE_SCALE - 1) / ENDURANCE_SCALE; + } + + /// + /// Get empty hole number + /// + public int GetEmptyHoleNum() + { + int count = 0; + foreach (int hole in Holes) + { + if (hole == 0) + count++; + } + return count; + } + + /// + /// Get repair cost + /// + public int GetRepairCost() + { + if (MaxEndurance == 0 || MaxEndurance == CurEndurance) + return 0; + + int cost = (int)GetRawRepairCost(); + if (cost < 1) + cost = 1; + + return cost; + } + + /// + /// Get raw repair cost + /// + public float GetRawRepairCost() + { + float cost = 0.0f; + + if (!IsRepairable()) + return cost; + + if (CurEndurance == 0) + { + cost = RepairFee; + } + else if (MaxEndurance > 0 && CurEndurance < MaxEndurance) + { + float factor = (MaxEndurance - CurEndurance) / (float)MaxEndurance; + cost = RepairFee * factor; + } + + return cost; + } + + /// + /// Get scaled item price + /// + public int GetScaledPrice() + { + if (ScaleType != SCALE_SELL) + return (int)(Price * Count * PriceScale + 0.5f); + + int price = Price * Count; + + if (MaxEndurance == CurEndurance || MaxEndurance == 0) + return (int)(price * PriceScale + 0.5f); + else + return (int)(price * PriceScale * CurEndurance / (float)MaxEndurance + 0.5f); + } + + /// + /// Check if item is repairable + /// + public bool IsRepairable() + { + return MaxEndurance > 0; + } + + /// + /// Check if item is destroying + /// + public bool IsDestroying() + { + return CurEndurance == 0 && MaxEndurance > 0; + } + + /// + /// Check if item is rare + /// + public bool IsRare() + { + return RefineLvl >= 3; + } + + #endregion + + #region Property System + + /// + /// Get property effect essence flags + /// + public uint PropEffectEssence() + { + uint flags = 0; + foreach (Property prop in Props) + { + flags = PropEffectMask(prop, flags); + } + return flags; + } + + /// + /// Get property effect mask for a specific property + /// + public uint PropEffectMask(Property prop, uint flags) + { + // Get property type from game's item ext prop table + byte propType = GetPropertyType(prop.Type); + + switch (propType) + { + case 0: // Physical damage + flags |= PEE_PHYDAMAGE; + break; + case 1: // Max physical damage + flags |= PEE_PHYDAMAGE; + break; + case 3: // Magic damage + flags |= PEE_MAGICDAMAGE; + break; + case 6: // +Def -Atk + flags |= PEE_PHYDAMAGE; + flags |= PEE_PHYDEF; + break; + case 7: // +Atk -Def + flags |= PEE_PHYDAMAGE; + flags |= PEE_PHYDEF; + break; + case 8: // +Magic -MagicDef + flags |= PEE_MAGICDAMAGE; + flags |= PEE_GOLDDEF; + flags |= PEE_WOODDEF; + flags |= PEE_WATERDEF; + flags |= PEE_FIREDEF; + flags |= PEE_EARTHDEF; + break; + case 9: // Attack speed + flags |= PEE_ATKSPEED; + break; + case 10: // Attack distance + flags |= PEE_ATKDIST; + break; + case 12: // Physical defense + flags |= PEE_PHYDEF; + break; + case 14: // All magic defense + flags |= PEE_GOLDDEF; + flags |= PEE_WOODDEF; + flags |= PEE_WATERDEF; + flags |= PEE_FIREDEF; + flags |= PEE_EARTHDEF; + break; + case 15: // Gold defense + flags |= PEE_GOLDDEF; + break; + case 17: // Wood defense + flags |= PEE_WOODDEF; + break; + case 19: // Water defense + flags |= PEE_WATERDEF; + break; + case 21: // Fire defense + flags |= PEE_FIREDEF; + break; + case 23: // Earth defense + flags |= PEE_EARTHDEF; + break; + case 30: // +Gold -Fire + flags |= PEE_GOLDDEF; + flags |= PEE_FIREDEF; + break; + case 31: // +Wood -Gold + flags |= PEE_GOLDDEF; + flags |= PEE_WOODDEF; + break; + case 32: // +Water -Earth + flags |= PEE_WATERDEF; + flags |= PEE_EARTHDEF; + break; + case 33: // +Fire -Water + flags |= PEE_WATERDEF; + flags |= PEE_FIREDEF; + break; + case 34: // +Earth -Wood + flags |= PEE_WOODDEF; + flags |= PEE_EARTHDEF; + break; + case 35: // HP + flags |= PEE_HP; + break; + case 36: // MP + flags |= PEE_MP; + break; + case 50: // Dodge + flags |= PEE_DODGE; + break; + case 53: // Endurance (%) + flags |= PEE_ENDURANCE; + break; + case 56: // Equipment requirements + flags |= PEE_STRENGTHREQ; + flags |= PEE_AGILITYREQ; + flags |= PEE_ENERGYREQ; + flags |= PEE_VITALITYREQ; + break; + case 92: // +Physical +Magic + flags |= PEE_PHYDEF; + flags |= PEE_GOLDDEF; + flags |= PEE_WOODDEF; + flags |= PEE_WATERDEF; + flags |= PEE_FIREDEF; + flags |= PEE_EARTHDEF; + break; + } + + return flags; + } + + /// + /// Get property type from property ID + /// + private static Dictionary s_propIdToType; + private static bool s_propMapLoaded; + private static readonly object s_propLock = new object(); + + private static void EnsurePropMapLoaded() + { + if (s_propMapLoaded) return; + lock (s_propLock) + { + if (s_propMapLoaded) return; + s_propIdToType = new Dictionary(); + + try + { + // Parse configs/item_ext_prop.txt to learn valid property type ids + string path = Path.Combine(UnityEngine.Application.streamingAssetsPath, "configs/item_ext_prop.txt"); + if (File.Exists(path)) + { + foreach (var rawLine in File.ReadLines(path)) + { + string line = rawLine.Trim(); + if (line.Length == 0) continue; + if (line.StartsWith("//") || line.StartsWith("#")) continue; + + // Match forms like: 12: or 12\t + var m = Regex.Match(line, "^(?\\d{1,4})\\s*[:\\t\\s]"); + if (m.Success) + { + if (int.TryParse(m.Groups["id"].Value, out int id) && id >= 0 && id <= 255) + { + // Treat the id as both propId and normalized type for lack of a separate map. + if (!s_propIdToType.ContainsKey(id)) + s_propIdToType[id] = (byte)id; + } + } + } + } + } + catch { /* ignore parse errors; fallback below will handle */ } + + s_propMapLoaded = true; + } + } + + private byte GetPropertyType(int propId) + { + // Load once from item_ext_prop.txt; if not enough info, fall back to heuristics + EnsurePropMapLoaded(); + + if (propId >= 0 && propId <= 255) + { + if (s_propIdToType != null && s_propIdToType.TryGetValue(propId, out byte t)) + return t; + return (byte)propId; // identity mapping for common types listed 0..255 + } + + // Refine codes and other extended ranges from original client + if (propId >= 200 && propId <= 212) return (byte)propId; // refine buckets + if (propId >= 100 && propId <= 115) return (byte)propId; // sharpener/stone buckets + + // Unknown + return 0xff; + } + + /// + /// Set properties to local + /// + public void SetLocalProps() + { + if (Props.Count == 0) + return; + + foreach (Property prop in Props) + { + prop.Local = true; + } + } + + /// + /// Get deadly strike rate provided by this equipment + /// + public int GetDeadlyStrikeRate(bool suiteGen) + { + int val = 0; + + if (suiteGen) + { + // Suite generation logic would go here + // This is a simplified version + } + else + { + foreach (Property prop in Props) + { + byte propType = GetPropertyType(prop.Type); + if (propType == 45 || propType == 110) + val += prop.Params[0]; + } + } + + return val; + } + + /// + /// Decide equipment name color + /// + public int DecideNameCol() + { + int index = GetColorStrID(TemplateId); + if (index >= 0) + return index; + + int col = ITEMDESC_COL_WHITE; + + switch (FixProps) + { + case 1: col = ITEMDESC_COL_GREEN; break; + case 2: col = ITEMDESC_COL_YELLOW; break; + case 3: col = ITEMDESC_COL_DARKGOLD; break; + default: + if (PropNum > 0) + col = ITEMDESC_COL_LIGHTBLUE; + break; + } + + return col; + } + + /// + /// Get color string ID for template + /// + private int GetColorStrID(int templateId) + { + // This would normally query the game's color system + return -1; + } + + #endregion + + #region Description Methods + + /// + /// Get item description for booth buying + /// + public string GetBoothBuyDesc() + { + m_strDesc = ""; + + int white = ITEMDESC_COL_WHITE; + + // Item name + AddDescText(white, true, GetItemDescString(ITEMDESC_NAME), GetName()); + + // Base stats from element data + string baseStats = GetBaseStatsDesc(); + if (!string.IsNullOrEmpty(baseStats)) + { + m_strDesc += GetColorString(ITEMDESC_COL_WHITE); + m_strDesc += baseStats; + m_strDesc += "\\r"; + } + + // Price + AddPriceDesc(white, false); + + return m_strDesc; + } + + /// + /// Add price description + /// + public void AddPriceDesc(int col, bool repair) + { + if (repair) + AddDescText(col, false, GetItemDescString(ITEMDESC_REPAIRCOST), GetRepairCost()); + else + AddDescText(col, true, "Price: {0}", GetScaledPrice()); + } + + /// + /// Format property description + /// + public string FormatPropDesc(Property prop) + { + string tmpDesc = m_strDesc; + m_strDesc = ""; + + AddOneAddOnPropDesc(prop.Type, prop.Params, null, null, prop.Local); + + string result = m_strDesc; + m_strDesc = tmpDesc; + return result; + } + + /// + /// Format refine data into a string + /// + public string FormatRefineData(uint addonId) + { + // This would normally query the equipment addon data + // For now, return empty string + return ""; + } + + /// + /// Check property range value + /// + public bool CheckPropRangeValue(Property prop) + { + int idProp = prop.Type; + byte propType = GetPropertyType(idProp); + + switch (propType) + { + case 25: // +Gold(%) -Fire(%) + case 26: // +Wood(%) -Gold(%) + case 27: // +Water(%) -Earth(%) + case 28: // +Fire(%) -Water(%) + case 29: // +Earth(%) -Wood(%) + case 30: // +Gold -Fire + case 31: // +Wood -Gold + case 32: // +Water -Earth + case 33: // +Fire -Water + case 34: // +Earth -Wood + case 55: // Add skill + return false; + } + + // Check for range values in equipment addon data + // This would normally query the equipment addon data + return false; + } + + #endregion + + #region Helper Methods + + /// + /// Parse properties + /// + private void ParseProperties() + { + RefineLvl = 0; + PropNum = 0; + EmbedNum = 0; + + if (Props.Count == 0) + return; + + int level = 0; + + foreach (Property prop in Props) + { + if (prop.Embed) + { + EmbedNum++; + continue; + } + else if (prop.Suite) + continue; + else if (prop.Engraved) + continue; + + byte propType = GetPropertyType(prop.Type); + + switch (propType) + { + case 200: // Refine physical damage + case 201: // Refine magic damage + case 202: // Refine physical defense + case 203: // Refine gold defense + case 204: // Refine wood defense + case 205: // Refine water defense + case 206: // Refine fire defense + case 207: // Refine earth defense + case 208: // Refine HP + case 209: // Refine dodge + case 210: // Refine all magic defense + case 211: // Refine physical damage & magic damage + case 212: // Refine physical defense & magic defense + level = prop.Params[1]; + break; + } + + if (level > 0) + RefineLvl = level; + else + PropNum++; + } + } + + /// + /// Check if this equipment belongs to a suite + /// + public int GetSuiteID() + { + // This would normally query the game's suite equip table + return 0; + } + + /// + /// Get item description string by ID + /// + private string GetItemDescString(int id) + { + var tab = EC_Game.GetItemDesc(); + if (tab != null && tab.IsInitialized()) + { + string s = tab.GetString(id); + if (!string.IsNullOrEmpty(s)) return s; + } + // Fallback labels for common IDs when the table lacks #_index entries + switch (id) + { + case ITEMDESC_ADDPHYDAMAGE: return "Công vật lý"; + case ITEMDESC_ADDMAGICDAMAGE: return "TC phép thuật"; + case ITEMDESC_PHYDEFENCE: return "Thủ vật lý"; + case ITEMDESC_GOLDDEFENCE: return "Kháng Kim"; + case ITEMDESC_WOODDEFENCE: return "Kháng Mộc"; + case ITEMDESC_WATERDEFENCE: return "Kháng Thủy"; + case ITEMDESC_FIREDEFENCE: return "Kháng Hỏa"; + case ITEMDESC_EARTHDEFENCE: return "Kháng Thổ"; + case ITEMDESC_ATKTIME: return "Tốc độ TC(lần/giây)"; + case ITEMDESC_ADDATKDIST: return "Phạm vi tấn công"; + case ITEMDESC_NAME: return "Tên"; + default: + return $"String_{id}"; + } + } + + /// + /// Add description text + /// + private void AddDescText(int color, bool newLine, string format, params object[] args) + { + if (color >= 0) + { + // Add color formatting + string colorStr = GetColorString(color); + m_strDesc += colorStr; + } + + if (args.Length > 0) + m_strDesc += string.Format(format, args); + else + m_strDesc += format; + + if (newLine) + m_strDesc += "\\r"; + } + + /// + /// Get color string for color ID + /// + private string GetColorString(int colorId) + { + switch (colorId) + { + case ITEMDESC_COL_WHITE: return "^FF"; + case ITEMDESC_COL_GREEN: return "^GF"; + case ITEMDESC_COL_YELLOW: return "^YF"; + case ITEMDESC_COL_DARKGOLD: return "^DF"; + case ITEMDESC_COL_LIGHTBLUE: return "^BF"; + case ITEMDESC_COL_CYANINE: return "^CF"; + case ITEMDESC_COL_RED: return "^RF"; + case ITEMDESC_COL_GRAY: return "^GF"; + default: return "^FF"; + } + } + + /// + /// Convert word color to Unity Color + /// + private Color ColorFromWord(ushort wordColor) + { + // Convert word color to Unity Color + // This is a simplified conversion + return new Color( + ((wordColor >> 10) & 0x1F) / 31.0f, + ((wordColor >> 5) & 0x1F) / 31.0f, + (wordColor & 0x1F) / 31.0f, + 1.0f + ); + } + + /// + /// Check if property is sharpener property + /// + private bool IsSharpenerProperty(byte propType) + { + return propType >= 100 && propType <= 115; + } + + /// + /// Visualize float percent + /// + private int VisualizeFloatPercent(int value) + { + return value / 100; + } + + /// + /// Add one add-on property description + /// + private void AddOneAddOnPropDesc(int idProp, int[] param, int[] aPEEVals, int[] aRefines, bool local) + { + // This is a simplified version - the full implementation would handle all property types + byte propType = GetPropertyType(idProp); + + if (!IsSharpenerProperty(propType)) + { + switch (propType) + { + case 0: // Physical damage + if (!local && aPEEVals != null) + aPEEVals[PEEI_PHYDAMAGE] += param[0]; + AddDescText(-1, false, GetItemDescString(ITEMDESC_ADDPHYDAMAGE)); + AddDescText(-1, true, " %+d", param[0]); + break; + case 3: // Magic damage + if (!local && aPEEVals != null) + aPEEVals[PEEI_MAGICDAMAGE] += param[0]; + AddDescText(-1, false, GetItemDescString(ITEMDESC_ADDMAGICDAMAGE)); + AddDescText(-1, true, " %+d", param[0]); + break; + case 12: // Physical defense + if (!local && aPEEVals != null) + aPEEVals[PEEI_PHYDEF] += param[0]; + AddDescText(-1, false, GetItemDescString(ITEMDESC_PHYDEFENCE)); + AddDescText(-1, true, " %+d", param[0]); + break; + case 35: // HP + if (!local && aPEEVals != null) + aPEEVals[PEEI_HP] += param[0]; + AddDescText(-1, false, GetItemDescString(ITEMDESC_ADDHP)); + AddDescText(-1, true, " %+d", param[0]); + break; + case 36: // MP + if (!local && aPEEVals != null) + aPEEVals[PEEI_MP] += param[0]; + AddDescText(-1, false, GetItemDescString(ITEMDESC_ADDMP)); + AddDescText(-1, true, " %+d", param[0]); + break; + case 50: // Dodge + if (!local && aPEEVals != null) + aPEEVals[PEEI_DODGE] += param[0]; + AddDescText(-1, false, GetItemDescString(ITEMDESC_DODGE)); + AddDescText(-1, true, " %+d", param[0]); + break; + default: + AddDescText(-1, true, GetItemDescString(ITEMDESC_ERRORPROP), idProp); + break; + } + } + } + + /// + /// Build add-ons properties description + /// + private void BuildAddOnPropDesc(int[] aPEEVals, int[] aRefines) + { + if (Props.Count == 0) + return; + + // Change color + m_strDesc += GetColorString(ITEMDESC_COL_LIGHTBLUE); + + foreach (Property prop in Props) + { + // Properties added by Embedded stone will be printed by BuildTesseraDesc() later + // Ignore suite properties also + if (prop.Embed || prop.Suite || prop.Engraved) + continue; + + AddOneAddOnPropDesc(prop.Type, prop.Params, aPEEVals, aRefines, prop.Local); + } + } + + /// + /// Get soul power added by this equipment + /// + public int GetSoulPowerAdded() + { + int added = 0; + int propertyCount = Props.Count; + + foreach (Property prop in Props) + { + if (prop.NumParam > 0) + { + byte propType = GetPropertyType(prop.Type); + if (propType == 63) // Soul power property + { + added += prop.Params[0]; + } + } + } + + return added; + } + + /// + /// Build tessera description + /// + private void BuildTesseraDesc() + { + if (Holes.Count == 0) + return; + + int cyanine = ITEMDESC_COL_CYANINE; + + foreach (int hole in Holes) + { + if (hole == 0) + continue; + + // Get item name + string itemName = EC_IvtrItem.ResolveItemName(hole); + string descText = "null"; + + // This would normally check if it's a stone and get its description + // For now, use the item name + descText = itemName; + + AddDescText(cyanine, true, GetItemDescString(ITEMDESC_2STRINGS), itemName, descText); + } + } + + /// + /// Add suite description + /// + public void AddSuiteDesc() + { + int idSuite = GetSuiteID(); + if (idSuite == 0) + return; // This equipment isn't one of any suite + + // This would normally build the suite description + // For now, just add a placeholder + m_strDesc += "\\r\\r"; + AddDescText(ITEMDESC_COL_YELLOW, true, "Suite Equipment"); + } + + /// + /// Add destroying description + /// + public void AddDestroyingDesc(int dropItemID, int num) + { + if (!IsDestroying() || dropItemID == 0 || num == 0) + return; + + // This would normally get the destroying info from element data + int red = ITEMDESC_COL_RED; + + m_strDesc += "\\r"; + AddDescText(red, true, GetItemDescString(ITEMDESC_EQUIP_DESTROYING)); + + // This would normally get the item name from element data + AddDescText(red, true, GetItemDescString(ITEMDESC_EQUIP_REPAIR_NEED_ITEM), "Repair Item"); + AddDescText(red, true, GetItemDescString(ITEMDESC_EQUIP_REPAIR_NEED_ITEMCNT), (int)(Math.Ceiling(num * 1.2))); + } + + /// + /// Add reputation requirement description + /// + public void AddReputationReqDesc() + { + if (ReputationReq > 0) + { + // This would normally check the host player's reputation + int col = ITEMDESC_COL_WHITE; // Default to white, would check against player reputation + AddDescText(col, true, GetItemDescString(ITEMDESC_REPUTATION_REQ), ReputationReq); + } + } + + /// + /// Get engraved property number + /// + public int GetEngravedPropertyNum() + { + int num = 0; + foreach (Property prop in Props) + { + if (prop.Engraved) + num++; + } + return num; + } + + /// + /// Get preview info + /// + public string GetPreviewInfo() + { + m_strDesc = ""; + BuildAddOnPropDesc(null, null); + return m_strDesc; + } + + /// + /// Get add-on property description + /// + public string GetAddOnPropDesc(int num, int[] addons) + { + int[] aPEEVals = new int[MAX_PEEINDEX]; + int[] aRefines = new int[MAX_REFINEINDEX]; + + m_strDesc = ""; + if (Props.Count == 0) + return m_strDesc; + + // Change color + m_strDesc += GetColorString(ITEMDESC_COL_LIGHTBLUE); + + foreach (Property prop in Props) + { + if (prop.Embed || prop.Suite || prop.Engraved) + continue; + + for (int j = 0; j < num; j++) + { + if ((addons[j] & 0x1fff) == prop.Type) + { + AddOneAddOnPropDesc(prop.Type, prop.Params, aPEEVals, aRefines, prop.Local); + break; + } + } + } + + return m_strDesc; + } + + /// + /// Get engrave description + /// + public string GetEngraveDesc() + { + int[] aPEEVals = new int[MAX_PEEINDEX]; + int[] aRefines = new int[MAX_REFINEINDEX]; + + m_strDesc = ""; + if (Props.Count == 0) + return m_strDesc; + + // Change color + m_strDesc += GetColorString(ITEMDESC_COL_YELLOW); + + foreach (Property prop in Props) + { + if (prop.Engraved) + AddOneAddOnPropDesc(prop.Type, prop.Params, aPEEVals, aRefines, prop.Local); + } + + return m_strDesc; + } + + #endregion + } + + /// + /// Equipment addon description class + /// + public class EC_IvtrEquipAddonDesc + { + private object m_pAddon; + private string m_strDesc = ""; + + /// + /// Set addon + /// + public bool SetAddon(uint idEquipAddon) + { + if (idEquipAddon == 0) + return false; + + // This would normally get the addon data from element data manager + // For now, return false + return false; + } + } +} \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_IvtrEquip.cs.meta b/Assets/PerfectWorld/Scripts/Managers/EC_IvtrEquip.cs.meta new file mode 100644 index 0000000000..71ac740d78 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Managers/EC_IvtrEquip.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 50210c0839c503b42843db0237a9c3a8 \ No newline at end of file