Files
test/Assets/PerfectWorld/Scripts/Managers/EC_IvtrEquip.cs
T
2025-10-21 16:55:57 +07:00

1653 lines
59 KiB
C#

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
{
/// <summary>
/// Equipment item class that handles all equipment-specific functionality
/// Converted from C++ EC_IvtrEquip.cpp
/// </summary>
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<int> Holes { get; set; }
public List<Property> Props { get; set; }
// Description
protected string m_strDesc = "";
#endregion
#region Base Stats (from Element Data)
private static bool TryGetNumber<T>(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<int>(data, new[] { "damage_low" }, out dmgLow);
bool hasDmgHighMax = TryGetNumber<int>(data, new[] { "damage_high_max", "damage_high" }, out dmgHighMax);
List<string> lines = new List<string>();
if (hasDmgLow && hasDmgHighMax)
{
// Physical damage range
lines.Add(string.Format("{0} {1}-{2}", GetItemDescString(ITEMDESC_ADDPHYDAMAGE), dmgLow, dmgHighMax));
// Magic damage if available
if (TryGetNumber<int>(data, new[] { "magic_damage_low" }, out int mdLow) &&
TryGetNumber<int>(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<float>(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<float>(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<float>(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<int>(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<int>(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<int>();
Props = new List<Property>();
}
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<int>(other.Holes);
Props = new List<Property>(other.Props.Select(p => new Property(p)));
}
#endregion
#region Core Methods
/// <summary>
/// Set item detail information from binary data
/// </summary>
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; }
}
/// <summary>
/// Read maker information from binary data
/// </summary>
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 = "";
}
}
/// <summary>
/// Set new mark with color
/// </summary>
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
/// <summary>
/// Get item name
/// </summary>
public string GetName()
{
return EC_IvtrItem.ResolveItemName(TemplateId);
}
/// <summary>
/// Add current endurance
/// </summary>
public int AddCurEndurance(int value)
{
CurEndurance += value;
CurEndurance = Mathf.Clamp(CurEndurance, 0, MaxEndurance);
return CurEndurance;
}
/// <summary>
/// Convert endurance real value to displaying value
/// </summary>
public static int VisualizeEndurance(int v)
{
return (v + ENDURANCE_SCALE - 1) / ENDURANCE_SCALE;
}
/// <summary>
/// Get empty hole number
/// </summary>
public int GetEmptyHoleNum()
{
int count = 0;
foreach (int hole in Holes)
{
if (hole == 0)
count++;
}
return count;
}
/// <summary>
/// Get repair cost
/// </summary>
public int GetRepairCost()
{
if (MaxEndurance == 0 || MaxEndurance == CurEndurance)
return 0;
int cost = (int)GetRawRepairCost();
if (cost < 1)
cost = 1;
return cost;
}
/// <summary>
/// Get raw repair cost
/// </summary>
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;
}
/// <summary>
/// Get scaled item price
/// </summary>
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);
}
/// <summary>
/// Check if item is repairable
/// </summary>
public bool IsRepairable()
{
return MaxEndurance > 0;
}
/// <summary>
/// Check if item is destroying
/// </summary>
public bool IsDestroying()
{
return CurEndurance == 0 && MaxEndurance > 0;
}
/// <summary>
/// Check if item is rare
/// </summary>
public bool IsRare()
{
return RefineLvl >= 3;
}
#endregion
#region Property System
/// <summary>
/// Get property effect essence flags
/// </summary>
public uint PropEffectEssence()
{
uint flags = 0;
foreach (Property prop in Props)
{
flags = PropEffectMask(prop, flags);
}
return flags;
}
/// <summary>
/// Get property effect mask for a specific property
/// </summary>
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;
}
/// <summary>
/// Get property type from property ID
/// </summary>
private static Dictionary<int, byte> 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<int, byte>();
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: <text> or 12\t<text>
var m = Regex.Match(line, "^(?<id>\\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;
}
/// <summary>
/// Set properties to local
/// </summary>
public void SetLocalProps()
{
if (Props.Count == 0)
return;
foreach (Property prop in Props)
{
prop.Local = true;
}
}
/// <summary>
/// Get deadly strike rate provided by this equipment
/// </summary>
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;
}
/// <summary>
/// Decide equipment name color
/// </summary>
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;
}
/// <summary>
/// Get color string ID for template
/// </summary>
private int GetColorStrID(int templateId)
{
// This would normally query the game's color system
return -1;
}
#endregion
#region Description Methods
/// <summary>
/// Get item description for booth buying
/// </summary>
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;
}
/// <summary>
/// Add price description
/// </summary>
public void AddPriceDesc(int col, bool repair)
{
if (repair)
AddDescText(col, false, GetItemDescString(ITEMDESC_REPAIRCOST), GetRepairCost());
else
AddDescText(col, true, "Price: {0}", GetScaledPrice());
}
/// <summary>
/// Format property description
/// </summary>
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;
}
/// <summary>
/// Format refine data into a string
/// </summary>
public string FormatRefineData(uint addonId)
{
// This would normally query the equipment addon data
// For now, return empty string
return "";
}
/// <summary>
/// Check property range value
/// </summary>
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
/// <summary>
/// Parse properties
/// </summary>
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++;
}
}
/// <summary>
/// Check if this equipment belongs to a suite
/// </summary>
public int GetSuiteID()
{
// This would normally query the game's suite equip table
return 0;
}
/// <summary>
/// Get item description string by ID
/// </summary>
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}";
}
}
/// <summary>
/// Add description text
/// </summary>
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";
}
/// <summary>
/// Get color string for color ID
/// </summary>
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";
}
}
/// <summary>
/// Convert word color to Unity Color
/// </summary>
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
);
}
/// <summary>
/// Check if property is sharpener property
/// </summary>
private bool IsSharpenerProperty(byte propType)
{
return propType >= 100 && propType <= 115;
}
/// <summary>
/// Visualize float percent
/// </summary>
private int VisualizeFloatPercent(int value)
{
return value / 100;
}
/// <summary>
/// Add one add-on property description
/// </summary>
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;
}
}
}
/// <summary>
/// Build add-ons properties description
/// </summary>
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);
}
}
/// <summary>
/// Get soul power added by this equipment
/// </summary>
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;
}
/// <summary>
/// Build tessera description
/// </summary>
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);
}
}
/// <summary>
/// Add suite description
/// </summary>
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");
}
/// <summary>
/// Add destroying description
/// </summary>
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)));
}
/// <summary>
/// Add reputation requirement description
/// </summary>
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);
}
}
/// <summary>
/// Get engraved property number
/// </summary>
public int GetEngravedPropertyNum()
{
int num = 0;
foreach (Property prop in Props)
{
if (prop.Engraved)
num++;
}
return num;
}
/// <summary>
/// Get preview info
/// </summary>
public string GetPreviewInfo()
{
m_strDesc = "";
BuildAddOnPropDesc(null, null);
return m_strDesc;
}
/// <summary>
/// Get add-on property description
/// </summary>
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;
}
/// <summary>
/// Get engrave description
/// </summary>
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
}
/// <summary>
/// Equipment addon description class
/// </summary>
public class EC_IvtrEquipAddonDesc
{
private object m_pAddon;
private string m_strDesc = "";
/// <summary>
/// Set addon
/// </summary>
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;
}
}
}