// Port of C++ CDlgCharacter (DlgCharacter.cpp / DlgCharacter.h) // Character information panel: stats, attributes, equipment bonuses, attribute point allocation. using System; using System.Collections.Generic; using System.Text; using BrewMonster.Network; using BrewMonster.Scripts; using BrewMonster.Scripts.Managers; using PerfectWorld.Scripts.Managers; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using TMPro; namespace BrewMonster.UI { public class DlgCharacter : AUIDialog { private const int INTERAL_LONG = 618; private const int INTERAL_SHORT = 100; #region UI References (same names as C++) [Header("Basic Info")] [SerializeField] private TextMeshProUGUI m_pTxt_CharName; [SerializeField] private TextMeshProUGUI m_pTxt_CharLevel; [SerializeField] private TextMeshProUGUI m_pTxt_Profession; [SerializeField] private TextMeshProUGUI m_pTxt_Faction; [SerializeField] private TextMeshProUGUI m_pTxt_Partner; [SerializeField] private TextMeshProUGUI m_pTxt_ExpCurrent; [SerializeField] private TextMeshProUGUI m_pTxt_ExpRequire; [SerializeField] private TextMeshProUGUI m_pTxt_Distinction; [SerializeField] private TextMeshProUGUI m_pTxt_xz; [Header("Resources")] [SerializeField] private TextMeshProUGUI m_pTxt_HP; [SerializeField] private TextMeshProUGUI m_pTxt_MP; [SerializeField] private TextMeshProUGUI m_pTxt_SP; [SerializeField] private TextMeshProUGUI m_pTxt_Point; [Header("Attributes")] [SerializeField] private TextMeshProUGUI m_pTxt_Str; [SerializeField] private TextMeshProUGUI m_pTxt_Agi; [SerializeField] private TextMeshProUGUI m_pTxt_Vit; [SerializeField] private TextMeshProUGUI m_pTxt_Int; [Header("Combat Stats")] [SerializeField] private TextMeshProUGUI m_pTxt_Attack; [SerializeField] private TextMeshProUGUI m_pTxt_AtkSpeed; [SerializeField] private TextMeshProUGUI m_pTxt_Definition; [SerializeField] private TextMeshProUGUI m_pTxt_Evade; [SerializeField] private TextMeshProUGUI m_pTxt_MoveSpeed; [SerializeField] private TextMeshProUGUI m_pTxt_MgcAttack; [SerializeField] private TextMeshProUGUI m_pTxt_Critical; [SerializeField] private TextMeshProUGUI m_pTxt_PhyDefense; [SerializeField] private TextMeshProUGUI m_pTxt_MgcDefense; [SerializeField] private TextMeshProUGUI m_pTxt_AttackLevel; [SerializeField] private TextMeshProUGUI m_pTxt_DefenseLevel; [SerializeField] private TextMeshProUGUI m_pTxt_CrtPower; [SerializeField] private TextMeshProUGUI m_pTxt_SoulPower; [Header("Hints / Additional")] [SerializeField] private TextMeshProUGUI m_pNew_PhyDefense; [SerializeField] private TextMeshProUGUI m_pNew_MgcDefense; [SerializeField] private TextMeshProUGUI m_pNew_AttackLevel; [SerializeField] private TextMeshProUGUI m_pNew_DefenseLevel; [SerializeField] private TextMeshProUGUI m_pTxt_Force; [SerializeField] private TextMeshProUGUI m_pTxt_Title; [SerializeField] private TextMeshProUGUI m_pTxt_Realm; [SerializeField] private TextMeshProUGUI m_pTxt_Vigour; [SerializeField] private TextMeshProUGUI m_pTxt_Stealth; [SerializeField] private TextMeshProUGUI m_pTxt_Antistealth; [SerializeField] private TextMeshProUGUI m_pTxt_Penetration; [SerializeField] private TextMeshProUGUI m_pTxt_Resilience; [SerializeField] private TextMeshProUGUI m_pNew_Vigour; [SerializeField] private TextMeshProUGUI m_pNew_SoulPower; [SerializeField] private TextMeshProUGUI m_pNew_Penetration; [SerializeField] private TextMeshProUGUI m_pNew_Resilience; [Header("Character Portrait")] [SerializeField] private RawImage m_pImg_Char; [Tooltip("Optional. Pivot transform to rotate (Y) when dragging on portrait. E.g. character model or camera rig.")] [SerializeField] private Transform m_pPortraitPivot; [Header("Realm Progress")] [SerializeField] private Slider m_pPro_RealmExp; [Header("Buttons")] [SerializeField] private Button m_pBtn_AddStr; [SerializeField] private Button m_pBtn_AddAgi; [SerializeField] private Button m_pBtn_AddVit; [SerializeField] private Button m_pBtn_AddInt; [SerializeField] private Button m_pBtn_MinusStr; [SerializeField] private Button m_pBtn_MinusAgi; [SerializeField] private Button m_pBtn_MinusVit; [SerializeField] private Button m_pBtn_MinusInt; [SerializeField] private Button m_pBtn_Confirm; [SerializeField] private Button m_pBtn_Reset; [SerializeField] private Button m_pBtn_Cancel; [SerializeField] private Button m_pBtn_Force; [SerializeField] private Button m_pBtn_TitleList; [SerializeField] private Button m_pBtn_Book; #endregion #region State (same as C++) private int m_nStatusPtUsed; private ROLEEXTPROP_BASE m_repBase; private int m_nMouseLastX; private int m_nMouseOffset; private int m_nMouseOffsetThis; private float m_dwStartTime; private float m_dwLastTime; private int m_iIntervalTime; private Button m_pButtonPress; private bool m_bAdd; private static readonly Color ColorBonus = new Color(0f, 1f, 0f); private static readonly Color ColorNormal = Color.white; private string m_cachedHostName = ""; #endregion public override void OnEnable() { base.OnEnable(); EventBus.Subscribe(OnInfoHostPlayer); } public override void OnDisable() { EventBus.Unsubscribe(OnInfoHostPlayer); base.OnDisable(); } private void OnInfoHostPlayer(CECHostPlayer.InfoHostPlayer e) { m_cachedHostName = e.NameHostPlayer ?? ""; } public override void Awake() { base.Awake(); SetName("Win_Character"); m_nMouseOffset = 0; m_nMouseOffsetThis = 0; m_iIntervalTime = INTERAL_LONG; m_dwLastTime = 0; m_pButtonPress = null; ResetPoints(); } public override void Start() { base.Start(); WireButtons(); WirePortraitDrag(); } private void WirePortraitDrag() { if (m_pImg_Char == null) return; var go = m_pImg_Char.gameObject; var receiver = go.GetComponent(); if (receiver == null) receiver = go.AddComponent(); receiver.Init(this); } /// Called by portrait drag receiver. deltaX in screen space; rotation clamped to [-70,70] degrees. public void OnPortraitDragDelta(float deltaX) { m_nMouseOffset += (int)deltaX; m_nMouseOffset = Mathf.Clamp(m_nMouseOffset, -70, 70); m_nMouseOffsetThis = m_nMouseOffset; if (m_pPortraitPivot != null) m_pPortraitPivot.localRotation = Quaternion.Euler(0f, m_nMouseOffset, 0f); } public override void Show(bool value) { m_bShow = value; base.Show(value); } private void WireButtons() { if (m_pBtn_AddStr != null) m_pBtn_AddStr.onClick.AddListener(() => OnAddAttr(m_pBtn_AddStr, 0)); // Str if (m_pBtn_AddAgi != null) m_pBtn_AddAgi.onClick.AddListener(() => OnAddAttr(m_pBtn_AddAgi, 1)); if (m_pBtn_AddVit != null) m_pBtn_AddVit.onClick.AddListener(() => OnAddAttr(m_pBtn_AddVit, 2)); // Vit maps to 3 in C++ iFourPro if (m_pBtn_AddInt != null) m_pBtn_AddInt.onClick.AddListener(() => OnAddAttr(m_pBtn_AddInt, 3)); // Int/Energy if (m_pBtn_MinusStr != null) m_pBtn_MinusStr.onClick.AddListener(OnMinusStr); if (m_pBtn_MinusAgi != null) m_pBtn_MinusAgi.onClick.AddListener(OnMinusAgi); if (m_pBtn_MinusVit != null) m_pBtn_MinusVit.onClick.AddListener(OnMinusVit); if (m_pBtn_MinusInt != null) m_pBtn_MinusInt.onClick.AddListener(OnMinusInt); if (m_pBtn_Confirm != null) m_pBtn_Confirm.onClick.AddListener(OnCommand_confirm); if (m_pBtn_Reset != null) m_pBtn_Reset.onClick.AddListener(OnCommand_reset); if (m_pBtn_Cancel != null) m_pBtn_Cancel.onClick.AddListener(OnCommand_CANCEL); if (m_pBtn_Force != null) m_pBtn_Force.onClick.AddListener(OnCommand_force); if (m_pBtn_TitleList != null) m_pBtn_TitleList.onClick.AddListener(OnCommand_Title); if (m_pBtn_Book != null) m_pBtn_Book.onClick.AddListener(OnCommand_ReincarnationBook); } public override bool Render() { RefreshHostDetails(); return base.Render(); } public override void Update() { OnTick(); base.Update(); } private void OnTick() { if (m_pButtonPress == null) return; if (!m_pButtonPress.gameObject.activeInHierarchy) { m_pButtonPress = null; return; } float now = Time.realtimeSinceStartup * 1000f; if (now - m_dwLastTime >= m_iIntervalTime) { if (m_bAdd) PropertyAdd(m_pButtonPress); else PropertyMinus(m_pButtonPress); m_dwLastTime += m_iIntervalTime; if (m_iIntervalTime == INTERAL_LONG) m_iIntervalTime = INTERAL_SHORT; } } private void OnAddAttr(Button btn, int kind) { PropertyAdd(btn); // Don't start continuous loop - only increment once per click m_pButtonPress = null; } private void OnMinusStr() { PropertyMinus(m_pBtn_MinusStr); } private void OnMinusAgi() { PropertyMinus(m_pBtn_MinusAgi); } private void OnMinusVit() { PropertyMinus(m_pBtn_MinusVit); } private void OnMinusInt() { PropertyMinus(m_pBtn_MinusInt); } public override void ResetPoints() { m_nStatusPtUsed = 0; m_repBase = new ROLEEXTPROP_BASE(false); m_repBase.vitality = 0; m_repBase.energy = 0; m_repBase.strength = 0; m_repBase.agility = 0; } private void PropertyAdd(Button btn) { CECHostPlayer host = GetHostPlayer(); if (host == null) return; ROLEBASICPROP rbp = host.GetBasicProps(); if (m_nStatusPtUsed >= rbp.iStatusPt) return; if (btn == m_pBtn_AddStr) { m_repBase.strength++; m_nStatusPtUsed++; } else if (btn == m_pBtn_AddAgi) { m_repBase.agility++; m_nStatusPtUsed++; } else if (btn == m_pBtn_AddVit) { m_repBase.vitality++; m_nStatusPtUsed++; } else if (btn == m_pBtn_AddInt) { m_repBase.energy++; m_nStatusPtUsed++; } } private void PropertyMinus(Button btn) { if (btn == m_pBtn_MinusStr && m_repBase.strength > 0) { m_repBase.strength--; m_nStatusPtUsed--; } else if (btn == m_pBtn_MinusAgi && m_repBase.agility > 0) { m_repBase.agility--; m_nStatusPtUsed--; } else if (btn == m_pBtn_MinusVit && m_repBase.vitality > 0) { m_repBase.vitality--; m_nStatusPtUsed--; } else if (btn == m_pBtn_MinusInt && m_repBase.energy > 0) { m_repBase.energy--; m_nStatusPtUsed--; } } private void OnCommand_confirm() { UnityGameSession.c2s_CmdSetStatusPts( m_repBase.vitality, m_repBase.energy, m_repBase.strength, m_repBase.agility); ResetPoints(); // Request fresh ext props so host player gets updated values from server; UI will refresh when response arrives UnityGameSession.c2s_SendCmdGetExtProps(); } private void OnCommand_reset() { ResetPoints(); } private void OnCommand_CANCEL() { CloseDialogue(); } private void OnCommand_force() { var pForce = m_pAUIManager?.GetDialog("Win_Force"); if (pForce != null) pForce.Show(!pForce.IsShow()); } private void OnCommand_Title() { var pDlg = GetGameUIMan()?.GetDialog("Win_TitleList"); if (pDlg != null) pDlg.Show(!pDlg.IsShow()); } private void OnCommand_ReincarnationBook() { CECHostPlayer host = GetHostPlayer(); if (host == null) return; var pDlg = m_pAUIManager?.GetDialog("Win_ReincarnationBook"); if (pDlg != null && host.GetReincarnationCount() > 0) pDlg.Show(!pDlg.IsShow()); } private void RefreshHostDetails() { if (!gameObject.activeSelf) return; CECHostPlayer host = GetHostPlayer(); if (host == null) return; ROLEBASICPROP rbp = host.GetBasicProps(); ROLEEXTPROP rep = host.GetExtendProps(); var gameRun = EC_Game.GetGameRun(); if (gameRun == null) return; string charName = host.GetName(); if (string.IsNullOrEmpty(charName)) charName = m_cachedHostName; if (string.IsNullOrEmpty(charName)) { var ri = UnityGameSession.Instance?.GetRoleInfo(); if (ri?.name != null && ri.name.ByteArray != null && ri.name.ByteArray.Length > 0) charName = Encoding.Unicode.GetString(ri.name.ByteArray); } SetText(m_pTxt_CharName, charName ?? ""); SetText(m_pTxt_CharLevel, rbp.iLevel.ToString()); SetText(m_pTxt_Profession, gameRun.GetProfName(host.GetProfession())); int idFaction = host.GetFactionID(); string factionName = idFaction <= 0 ? GetStringFromTable(251) : ""; var factionMan = EC_Game.GetFactionMan(); if (idFaction > 0 && factionMan != null) { var fi = factionMan.GetFaction((uint)idFaction, false); if (fi != null) factionName = fi.m_szName ?? ""; } SetText(m_pTxt_Faction, factionName); int spouse = 0; // TODO: host.GetSpouse() when exposed; use gameRun.GetPlayerName(spouse, false) when that API exists if (spouse != 0) { string spouseName = ""; // TODO: gameRun.GetPlayerName(spouse, false) SetText(m_pTxt_Partner, spouseName ?? ""); } else SetText(m_pTxt_Partner, GetStringFromTable(786)); int[] iFourPro = CalcEquipmentBonus(host); SetText(m_pTxt_ExpCurrent, rbp.iExp.ToString()); SetText(m_pTxt_ExpRequire, host.GetLevelUpExp(rbp.iLevel).ToString()); int reputation = 0; // TODO: host.GetReputation() when exposed SetText(m_pTxt_Distinction, reputation.ToString()); SetText(m_pTxt_xz, GetGameUIMan()?.GetStringFromTable(1001 + rbp.iLevel2) ?? ""); SetText(m_pTxt_HP, $"{rbp.iCurHP}/{rep.bs.max_hp}"); SetText(m_pTxt_MP, $"{rbp.iCurMP}/{rep.bs.max_mp}"); SetText(m_pTxt_SP, rbp.iSP.ToString()); SetText(m_pTxt_Point, (rbp.iStatusPt - m_nStatusPtUsed).ToString()); int strVal = rep.bs.strength + m_repBase.strength; SetText(m_pTxt_Str, strVal.ToString()); SetColor(m_pTxt_Str, iFourPro[0] != 0 ? ColorBonus : ColorNormal); int agiVal = rep.bs.agility + m_repBase.agility; SetText(m_pTxt_Agi, agiVal.ToString()); SetColor(m_pTxt_Agi, iFourPro[1] != 0 ? ColorBonus : ColorNormal); int vitVal = rep.bs.vitality + m_repBase.vitality; SetText(m_pTxt_Vit, vitVal.ToString()); SetColor(m_pTxt_Vit, iFourPro[3] != 0 ? ColorBonus : ColorNormal); int energyVal = rep.bs.energy + m_repBase.energy; SetText(m_pTxt_Int, energyVal.ToString()); SetColor(m_pTxt_Int, iFourPro[2] != 0 ? ColorBonus : ColorNormal); SetText(m_pTxt_Attack, $"{rep.ak.DamageLow}-{rep.ak.DamageHigh}"); string atkSpeedUnit = GetGameUIMan()?.GetStringFromTable(279) ?? "/s"; float atkSpeed = rep.ak.AttackSpeed != 0 ? 1f / (rep.ak.AttackSpeed * 0.05f) : 0f; SetText(m_pTxt_AtkSpeed, $"{atkSpeed:F2} {atkSpeedUnit}"); SetText(m_pTxt_Definition, rep.ak.Attack.ToString()); SetText(m_pTxt_Evade, rep.df.armor.ToString()); string moveUnit = GetGameUIMan()?.GetStringFromTable(280) ?? "m/s"; SetText(m_pTxt_MoveSpeed, $"{rep.mv.run_speed:F1} {moveUnit}"); SetText(m_pTxt_MgcAttack, $"{rep.ak.DamageMagicLow}-{rep.ak.DamageMagicHigh}"); SetText(m_pTxt_Critical, $"{rbp.iCritRate}%"); int nLevel = rbp.iLevel; float fReduce = DefenceToPercent(rep.df.defense, nLevel); SetText(m_pTxt_PhyDefense, rep.df.defense.ToString()); SetText(m_pNew_PhyDefense, string.Format(GetGameUIMan()?.GetStringFromTable(490) ?? "{0} {1}", nLevel, fReduce)); if (m_pTxt_MgcDefense != null || m_pNew_MgcDefense != null) { int nSum = 0; var parts = new System.Collections.Generic.List(); for (int i = 0; i < GameConstants.NUM_MAGICCLASS; i++) { nSum += rep.df.resistance[i]; float r = DefenceToPercent(rep.df.resistance[i], nLevel); parts.Add(string.Format(GetGameUIMan()?.GetStringFromTable(491 + i) ?? "", rep.df.resistance[i], nLevel, r)); } if (m_pTxt_MgcDefense != null) SetText(m_pTxt_MgcDefense, (nSum / GameConstants.NUM_MAGICCLASS).ToString()); if (m_pNew_MgcDefense != null) SetText(m_pNew_MgcDefense, string.Join("\n", parts)); } SetText(m_pTxt_AttackLevel, rbp.iAtkDegree.ToString()); SetText(m_pTxt_DefenseLevel, rbp.iDefDegree.ToString()); SetText(m_pTxt_CrtPower, $"{rbp.iCritDamageBonus + 200}%"); int soulPower = 0; // TODO: host.GetSoulPower() when exposed SetText(m_pTxt_SoulPower, soulPower >= 0 ? soulPower.ToString() : "-"); string soulHint = GetStringFromTable(8135); if (soulPower > 0 && host.GetProfession() == (int)PROFESSION.PROF_MONK) { soulHint = GetStringFromTable(8136) + "\n" + string.Format(GetStringFromTable(8130), soulPower) + "\n" + string.Format(GetStringFromTable(8131), (int)Mathf.Floor(soulPower * 0.08f * (1 + 0.01f * rbp.iAtkDegree))) + "\n" + string.Format(GetStringFromTable(8132), (int)Mathf.Floor(soulPower * 0.0002f + 1)) + "\n" + string.Format(GetStringFromTable(8133), soulPower * 0.0006f) + "\n" + string.Format(GetStringFromTable(8134), (int)Mathf.Floor(soulPower * 0.5f)); } SetText(m_pNew_SoulPower, soulHint); SetText(m_pTxt_Stealth, rbp.iInvisibleDegree.ToString()); SetText(m_pTxt_Antistealth, rbp.iAntiInvisibleDegree.ToString()); SetText(m_pTxt_Penetration, rbp.iPenetration.ToString()); SetText(m_pNew_Penetration, string.Format(GetStringFromTable(9380), (int)(100 * rbp.iPenetration * 3 / (float)(rbp.iPenetration + 300)))); SetText(m_pTxt_Resilience, rbp.iResilience.ToString()); SetText(m_pNew_Resilience, string.Format(GetStringFromTable(9381), nLevel, (int)(100 * rbp.iResilience / (float)(rbp.iResilience + nLevel)))); int vigourRealm = 0; int realmLevel = host.GetRealmLevel(); if (m_pTxt_Realm != null) m_pTxt_Realm.gameObject.SetActive(realmLevel > 0); if (m_pPro_RealmExp != null) m_pPro_RealmExp.gameObject.SetActive(realmLevel > 0); if (realmLevel > 0) { SetText(m_pTxt_Realm, GetRealmDisplayName(realmLevel)); if (m_pPro_RealmExp != null) { int requireExp = 0; // TODO: host.GetRealmRequireExp() when exposed if (requireExp > 0) { int realmExp = 0; // TODO: host.GetRealmExp() when exposed m_pPro_RealmExp.value = Mathf.Clamp01((float)Math.Max(realmExp, 0) / requireExp); SetText(m_pNew_Vigour, string.Format(GetGameUIMan()?.GetStringFromTable(11164) ?? "", realmExp, requireExp)); } else { m_pPro_RealmExp.value = 1f; } } } SetText(m_pTxt_Vigour, rbp.iVigour.ToString()); SetText(m_pNew_Vigour, string.Format(GetStringFromTable(11146), vigourRealm)); int forceId = host.GetForce(); if (m_pBtn_Force != null) m_pBtn_Force.interactable = forceId > 0; if (m_pTxt_Force != null) SetText(m_pTxt_Force, ""); // TODO: force name when GetForceMgr/GetForceData exposed if (m_pTxt_Title != null) SetText(m_pTxt_Title, ""); // TODO: title when GetCurrentTitle/GetTitleConfig exposed if (m_pBtn_Book != null) m_pBtn_Book.interactable = host.GetReincarnationCount() > 0; } private static void SetText(TextMeshProUGUI t, string s) { if (t != null) t.text = s ?? ""; } private static void SetColor(TextMeshProUGUI t, Color c) { if (t != null) t.color = c; } private static float DefenceToPercent(int def, int nLevel) { float f = 100f * def / (def + 40f * nLevel - 25f); return Mathf.Min(f, 95f); } private string GetRealmDisplayName(int realmLevel) { if (realmLevel <= 0) return ""; string s = GetGameUIMan()?.GetStringFromTable(11000 + realmLevel); return s ?? ""; } private int[] CalcEquipmentBonus(CECHostPlayer host) { int[] iFourPro = { 0, 0, 0, 0 }; var inv = host?.GetEquipment(); if (inv == null) return iFourPro; int equipSlots = InventoryConst.SIZE_ALL_EQUIPIVTR; for (int i = 0; i < equipSlots; i++) { var item = inv.GetItem(i); if (item is EC_IvtrEquip equ && equ.Props != null) { int n = equ.Props.Count; for (int j = 0; j < n; j++) { var pro = equ.Props[j]; int byPropType = pro.Type; // use Type as prop type; map via GetItemExtPropType if needed int val = (pro.Params != null && pro.Params.Length > 0) ? pro.Params[0] : 0; if (byPropType >= 41 && byPropType <= 44) iFourPro[byPropType - 41] += val; else if (byPropType >= 106 && byPropType <= 108) iFourPro[byPropType - 106] += val; else if (byPropType >= 95 && byPropType <= 98) iFourPro[byPropType - 95] += val; else if (byPropType >= 128 && byPropType <= 131) { int[] order = { 3, 0, 1, 2 }; iFourPro[order[byPropType - 128]] += val; } } } } return iFourPro; } } /// Forwards drag on the portrait RawImage to DlgCharacter for rotation. public class DlgCharacterPortraitDragReceiver : MonoBehaviour, IBeginDragHandler, IDragHandler { private DlgCharacter m_dlg; public void Init(DlgCharacter dlg) { m_dlg = dlg; } public void OnBeginDrag(PointerEventData eventData) { } public void OnDrag(PointerEventData eventData) { if (m_dlg != null) m_dlg.OnPortraitDragDelta(eventData.delta.x); } } }