lcy
6 小时以前 2e7d66b9b7d541d878b2220fa1e50552e7553708
Main/System/Battle/UIComp/BattleHeroInfoBar.cs
@@ -10,8 +10,6 @@
/// </summary>
public class BattleHeroInfoBar : MonoBehaviour
{
    #region 内部类
    /// <summary>
    /// 飘字信息配置
    /// </summary>
@@ -24,15 +22,18 @@
        public bool showBackground = false;
        public bool useBuffColor = false;  // 是否使用 Buff 颜色(从 FloatingConfig 读取)
        public bool isDebuff = false;      // 是否是负向 Buff(决定用哪个颜色)
        public bool isRage = false;
    }
    
    #endregion
    #region Inspector字段
    
    [Header("UI Components")]
    public Slider sliderHp;
    public Slider sliderSlowHp;
    public Slider sliderXp;
    public GameObject maxXpGO;
    public Slider sliderShield1;
    public Slider sliderShield2;
    public BasicHeroInfoContainer heroInfoContainer;
    public BattleTips textTips;
    
@@ -45,44 +46,89 @@
    public FloatingConfig followFloatingConfig;
    [Tooltip("不跟随角色的飘字配置(固定在战场节点)")]
    public FloatingConfig noFollowFloatingConfig;
    [Header("Settings")]
    public float PopUpInterval = 0.2f;
    #endregion
    #region 私有字段
    public FloatingConfig rageFloatingConfig;
    
    protected BattleObject battleObject;
    protected float timer = 0f;
    
    protected List<TipsInfo> messages = new List<TipsInfo>();
    protected List<BattleTips> tipsList = new List<BattleTips>();
    protected List<HB428_tagSCBuffRefresh> buffList = new List<HB428_tagSCBuffRefresh>();
    protected Tween hpTween;
    [SerializeField] ButtonEx buffInfoButton;
    protected Sequence hpTween;
    protected Tween xpTween;
    protected Tween shieldTween1;
    protected Tween shieldTween2;
    protected Sequence damageSequence;
    
    #endregion
    private Queue<BattleDmgInfo> damageUpdateQueue = new Queue<BattleDmgInfo>();
    // 飘字GCD相关
    private float tipsGCDTimer = 0f;
    private const int TIPS_GCD_FRAMES = 5;
    #region Unity生命周期
    // 全局血量记录(按战场guid组织,以最大PackUID为准,记录所有对象:施法者和受击者)
    public static Dictionary<string, ulong> largestPackUID = new Dictionary<string, ulong>();
    public static Dictionary<string, Dictionary<long, long>> largestPackUIDAllObjectsToHp = new Dictionary<string, Dictionary<long, long>>();
    public static Dictionary<string, Dictionary<long, long>> largestPackUIDAllObjectsMaxHp = new Dictionary<string, Dictionary<long, long>>();
    public static Dictionary<string, Dictionary<long, ulong>> objectLargestPackUID = new Dictionary<string, Dictionary<long, ulong>>();
    
    protected void OnDisable()
    {
        CleanupTips();
    }
    
    #endregion
    #region 公共方法 - 初始化
    public void SetBattleObject(BattleObject _battleObject)
    {
        battleObject = _battleObject;
        heroInfoContainer.SetHeroInfo(battleObject.teamHero);
        RefreshBuff(battleObject.buffMgr.GetBuffList());
        UpdateHP(battleObject.teamHero.curHp, battleObject.teamHero.curHp, battleObject.teamHero.maxHp, false);
        UpdateXP(battleObject.teamHero.rage, battleObject.teamHero.rage, 100, false);
        if (battleObject is HeroBattleObject heroBattleObject)
        {
            heroInfoContainer.SetHeroInfo(heroBattleObject.teamHero);
        }
        CleanupTips();
        InitBuff();
        var buffMgr = battleObject.GetBuffMgr();
        if (buffMgr != null) // 命格不有 buff
        {
            RefreshBuff(buffMgr.GetBuffIconList());
        }
        if (!battleObject.IsTianziBoss())
        {
            UpdateHP(battleObject.GetCurHp(), battleObject.GetCurHp(), battleObject.GetMaxHp(), false);
        }
        UpdateXP(battleObject.GetRage(), battleObject.GetRage(), 100, false);
        long shieldValue = buffMgr != null ? buffMgr.GetShieldValue() : 0; // 命格没有护盾
        long curHp = battleObject.GetCurHp();
        long maxHp = battleObject.GetMaxHp();
        // 记录设置前的护盾值
        float oldShield1Value = sliderShield1.value;
        float oldShield2Value = sliderShield2.value;
        // 护盾1的值 = min(当前血量 + 护盾值, maxHp) / maxHp
        float shield1Value = maxHp > 0 ? Mathf.Min((float)(curHp + shieldValue), (float)maxHp) / (float)maxHp : 0;
        // 护盾2的值 = max(当前血量 + 护盾值 - maxHp, 0) / maxHp
        float shield2Value = maxHp > 0 ? Mathf.Max((float)(curHp + shieldValue - maxHp), 0f) / (float)maxHp : 0;
        sliderShield1.value = shield1Value;
        sliderShield2.value = shield2Value;
        // 打印设置护盾时的状态
        // Debug.LogError($"[BattleHeroInfoBar.SetBattleObject] 设置护盾 - curHp: {curHp}, shieldValue: {shieldValue}, maxHp: {maxHp}, shield1前: {oldShield1Value}, shield1后: {shield1Value}, shield2前: {oldShield2Value}, shield2后: {shield2Value}");
    }
    protected void InitBuff()
    {
        for (int i = 0; i < buffCells.Count; i++)
        {
            buffCells[i].SetActive(false);
        }
    }
    
    public void SetActive(bool active)
@@ -90,58 +136,137 @@
        gameObject.SetActive(active);
    }
    
    #endregion
    #region 公共方法 - Buff管理
    public void RefreshBuff(List<HB428_tagSCBuffRefresh> datas)
    {
        if (buffCells.IsNullOrEmpty())
            return;
        RefreshBuffCells(buffCells, datas);
        for (int i = 0; i < buffCells.Count; i++)
        // check shield buff
        var buffMgr = battleObject.GetBuffMgr();
        long shieldValue = buffMgr != null ? buffMgr.GetShieldValue() : 0; // 命格没有护盾
        long curHp = battleObject.GetCurHp();
        long maxHp = battleObject.GetMaxHp();
        // 记录设置前的护盾值
        float oldShield1Value = sliderShield1.value;
        float oldShield2Value = sliderShield2.value;
        // 护盾1的值 = min(当前血量 + 护盾值, maxHp) / maxHp
        float shield1Value = maxHp > 0 ? Mathf.Min((float)(curHp + shieldValue), (float)maxHp) / (float)maxHp : 0;
        // 护盾2的值 = max(当前血量 + 护盾值 - maxHp, 0) / maxHp
        float shield2Value = maxHp > 0 ? Mathf.Max((float)(curHp + shieldValue - maxHp), 0f) / (float)maxHp : 0;
        sliderShield1.value = shield1Value;
        sliderShield2.value = shield2Value;
        // if (!battleObject.IsTianziBoss())
        // {
        //     UpdateHP(curHp, curHp, maxHp, false);
        // }
        // 打印刷新护盾时的状态
        // Debug.LogError($"[BattleHeroInfoBar.RefreshBuff] 设置护盾 - curHp: {curHp}, shieldValue: {shieldValue}, maxHp: {maxHp}, shield1前: {oldShield1Value}, shield1后: {shield1Value}, shield2前: {oldShield2Value}, shield2后: {shield2Value}");
    }
    protected void RefreshBuffCells(List<BattleBuffCell> cells, List<HB428_tagSCBuffRefresh> datas)
    {
        if (datas == null)
        {
            if (i < datas.Count)
            for (int i = 0; i < cells.Count; i++)
            {
                buffCells[i].SetActive(true);
                buffCells[i].Init(datas[i], OnBuffCellClicked);
                cells[i].SetActive(false);
            }
        }
        else
        {
            if (battleObject.battleField.battleSwitch.BuffIcon)
            {
                for (int i = 0; i < cells.Count; i++)
                {
                    var cell = cells[i];
                    if (i < datas.Count)
                    {
                        cell.SetActive(true);
                        HB428_tagSCBuffRefresh buffData = datas[i];
                        SkillConfig skillConfig = SkillConfig.Get((int)buffData.SkillID);
                        cell.Init(buffData, () =>
                        {
                            //  点击buff图标 显示buff描述/当前身上所有buff
                        });
                    }
                    else
                    {
                        cell.SetActive(false);
                    }
                }
                buffInfoButton.SetListener(() =>
                {
                    if (datas.IsNullOrEmpty()) return;
                    string clickBuffBattleName = battleObject?.battleField?.ToString();
                    if (clickBuffBattleName == BattleConst.StoryBattleField) return;
                    EventBroadcast.Instance.Broadcast(EventName.BATTLE_CLICK_BUFF, new BattleClickBuffData()
                    {
                        isMySide = battleObject?.Camp == BattleCamp.Red,
                        heroID = (battleObject as HeroBattleObject)?.teamHero?.heroId ?? 0,
                        skinID = (battleObject as HeroBattleObject)?.teamHero?.SkinID ?? 0,
                        datas = datas,
                    });
                });
            }
            else
            {
                buffCells[i].SetActive(false);
                for (int i = 0; i < cells.Count; i++)
                {
                    cells[i].SetActive(false);
                }
            }
        }
    }
    #endregion
    #region 公共方法 - 飘字管理
    /// <summary>
    /// 添加飘字到队列
    /// 添加飘字到队列(非伤害飘字)
    /// </summary>
    public void ShowTips(string message, bool useArtText = false, bool followCharacter = true, float scaleRatio = 1f)
    {
        messages.Add(new TipsInfo
        if (battleObject.battleField.battleSwitch.NonDamageTips)
        {
            message = message,
            useArtText = useArtText,
            followCharacter = followCharacter,
            scaleRatio = scaleRatio
        });
            messages.Add(new TipsInfo
            {
                message = message,
                useArtText = useArtText,
                followCharacter = followCharacter,
                scaleRatio = scaleRatio
            });
        }
    }
    /// <summary>
    /// 添加自定义飘字配置到队列
    /// 添加自定义飘字配置到队列(非伤害飘字)
    /// </summary>
    public void ShowTips(TipsInfo tipsInfo)
    {
        messages.Add(tipsInfo);
        //  BUFF飘字
        if (tipsInfo.useBuffColor)
        {
            if (battleObject.battleField.battleSwitch.BuffAction)
            {
                messages.Add(tipsInfo);
            }
        }
        else
        {
            //  非伤害飘字
            if (battleObject.battleField.battleSwitch.NonDamageTips)
            {
                messages.Add(tipsInfo);
            }
        }
    }
    #endregion
    #region 公共方法 - 数值更新
    
    /// <summary>
    /// 更新血量显示
@@ -155,16 +280,28 @@
        
        if (tween)
        {
            // 关键修复:先设置起始值,再播放动画到目标值
            sliderHp.value = fromValue;  // ← 这行是关键!
            hpTween = sliderHp.DOValue(targetValue, 0.3f).SetAutoKill(false);
            hpTween = DOTween.Sequence();
            // sliderHp.value = fromValue;
            float diff = targetValue - fromValue;
            float cost = Mathf.Lerp(0, 1f, diff);
            hpTween.Append(sliderHp.DOValue(targetValue, cost).SetAutoKill(false));
            hpTween.Join(sliderSlowHp.DOValue(targetValue, cost * 1.5f).SetAutoKill(false));
            hpTween.onComplete += () =>
            {
                sliderHp.value = targetValue;
                sliderSlowHp.value = targetValue;
            };
            // sliderSlowHp
            battleObject.battleField.battleTweenMgr.OnPlayTween(hpTween);
        }
        else
        {
            sliderHp.value = targetValue;
            sliderSlowHp.value = targetValue;
        }
    }
    /// <summary>
    /// !!!临时的用于天子更新血量显示,等接口完善后删除
@@ -172,6 +309,10 @@
    public void UpdateHP(float value)
    {
        sliderHp.value = value; 
        sliderSlowHp.value = value;
        bool IsTianziBoss = battleObject.IsTianziBoss();
        sliderShield1.SetActive(!IsTianziBoss);
        sliderShield2.SetActive(!IsTianziBoss);
        //Debug.Log("TianziDamageBar UpdateHP value:" + value);
    }
@@ -181,37 +322,269 @@
    public void UpdateXP(long fromXp, long toXp, long maxXp, bool tween = true)
    {
        KillTween(ref xpTween);
        float fromValue = (float)fromXp / (float)maxXp;
        float targetValue = (float)toXp / (float)maxXp;
        if (tween)
        {
            // 同样的修复
            // 伤血加一个缓冲血条,绿条瞬减,黄条缓慢减
            sliderXp.value = fromValue;
            xpTween = sliderXp.DOValue(targetValue, 0.2f).SetAutoKill(false);
            xpTween.OnComplete(() =>
            {
                maxXpGO.SetActive(toXp >= maxXp);
            });
            battleObject.battleField.battleTweenMgr.OnPlayTween(xpTween);
        }
        else
        {
            if (toXp >= maxXp)
            {
                maxXpGO.SetActive(true);
            }
            else
            {
                maxXpGO.SetActive(false);
            }
            sliderXp.value = targetValue;
        }
    }
    #endregion
    #region 公共方法 - 运行时更新
    /// <summary>
    /// 播放血条 护盾的变化
    /// </summary>
    public void UpdateDamage(BattleDmgInfo dmgInfo)
    {
        // 验证数据有效性,防止空引用
        if (dmgInfo?.battleHurtParam == null)
            return;
        // 检查受击者对象有效性
        if (dmgInfo.battleHurtParam.hurter?.hurtObj != null)
        {
            var hurtObj = dmgInfo.battleHurtParam.hurter.hurtObj;
            // 检查对象是否已被销毁
            if (hurtObj == null || hurtObj.Equals(null))
            {
                Debug.LogWarning($"[UpdateDamage] 受击者对象已被销毁,跳过伤害更新");
                return;
            }
            // 验证是否能安全获取 maxHp(间接检查 HeroBattleObject 内部状态)
            if (hurtObj is HeroBattleObject && hurtObj.GetMaxHp() <= 0)
            {
                Debug.LogWarning($"[UpdateDamage] 受击者 maxHp 无效,跳过伤害更新");
                return;
            }
        }
        // 检查施法者对象有效性
        if (dmgInfo.battleHurtParam.caster?.casterObj != null)
        {
            var casterObj = dmgInfo.battleHurtParam.caster.casterObj;
            // 检查对象是否已被销毁
            if (casterObj == null || casterObj.Equals(null))
            {
                Debug.LogWarning($"[UpdateDamage] 施法者对象已被销毁,跳过伤害更新");
                return;
            }
            // 验证是否能安全获取 maxHp
            if (casterObj is HeroBattleObject && casterObj.GetMaxHp() <= 0)
            {
                Debug.LogWarning($"[UpdateDamage] 施法者 maxHp 无效,跳过伤害更新");
                return;
            }
        }
        // 数据验证通过,加入队列
        damageUpdateQueue.Enqueue(dmgInfo);
    }
    /// <summary>
    /// 实际执行伤害更新(统一处理目标和施法者)
    /// </summary>
    private void ExecuteDamageUpdate(BattleDmgInfo dmgInfo)
    {
        KillTween(ref damageSequence);
        string guid = battleObject.battleField.guid;
        long objID = battleObject.ObjID;
        ulong currentPackUID = dmgInfo.battleHurtParam.packUID;
        // 获取该战场的对象PackUID字典(用于按角色+角色身份粒度判断)
        Dictionary<long, ulong> objPackUIDDict = null;
        if (objectLargestPackUID.ContainsKey(guid))
        {
            objPackUIDDict = objectLargestPackUID[guid];
        }
        long maxHp, fromHp, toHp, fromShield, toShield;
        // 优先判断当前InfoBar是否为受击者(血量变化总是体现在hurter里)
        BattleHurtObj hurter = dmgInfo.battleHurtParam.hurter;
        if (hurter?.hurtObj != null && hurter.hurtObj.ObjID == objID)
        {
            // 按对象+受击者身份检查PackUID,避免不同身份(hurter/caster)的包互相阻拦
            long hurterKey = objID * 2; // hurter用偶数key
            if (objPackUIDDict != null
                && objPackUIDDict.ContainsKey(hurterKey)
                && currentPackUID < objPackUIDDict[hurterKey])
            {
                // Debug.LogWarning($"[ExecuteDamageUpdate] 忽略旧包(受击者) - ObjID:{objID}, 当前PackUID:{currentPackUID} < 对象受击最大PackUID:{objPackUIDDict[hurterKey]}");
                return;
            }
            // 当前InfoBar是受击者(包括给自己治疗、给自己造成伤害的情况)
            if (hurter.hurtObj.IsTianziBoss())
            {
                return;
            }
            // 直接使用 dmgInfo 中的数据(已经被 CompareAndExchangeLargestPackUIDHp 验证过)
            maxHp = hurter.maxHp;
            fromHp = hurter.fromHp;
            toHp = hurter.toHp;
            fromShield = hurter.fromShieldValue;
            toShield = hurter.toShieldValue;
            // 更新该对象作为受击者的最大PackUID
            if (objPackUIDDict != null)
            {
                objPackUIDDict[hurterKey] = currentPackUID;
            }
            // Debug.LogError($"[ExecuteDamageUpdate] 受击者 - ObjID:{objID}, fromHp:{fromHp}, toHp:{toHp}, maxHp:{maxHp} (PackUID:{currentPackUID})");
        }
        // 其次判断是否为施法者(施法消耗生命等情况)
        else
        {
            BattleCastObj caster = dmgInfo.battleHurtParam.caster;
            if (caster?.casterObj == null || caster.casterObj.ObjID != objID)
            {
                // Debug.LogWarning($"[ExecuteDamageUpdate] 当前对象 {objID} 既不是施法者也不是受击者");
                return;
            }
            // 按对象+施法者身份检查PackUID
            long casterKey = objID * 2 + 1; // caster用奇数key
            if (objPackUIDDict != null
                && objPackUIDDict.ContainsKey(casterKey)
                && currentPackUID < objPackUIDDict[casterKey])
            {
                // Debug.LogWarning($"[ExecuteDamageUpdate] 忽略旧包(施法者) - ObjID:{objID}, 当前PackUID:{currentPackUID} < 对象施法最大PackUID:{objPackUIDDict[casterKey]}");
                return;
            }
            if (caster.casterObj.IsTianziBoss())
            {
                return;
            }
            // 直接使用 dmgInfo 中的数据(已经被 CompareAndExchangeLargestPackUIDHp 验证过)
            maxHp = caster.maxHp;
            fromHp = caster.fromHp;
            toHp = caster.toHp;
            fromShield = caster.fromShieldValue;
            toShield = caster.toShieldValue;
            // 更新该对象作为施法者的最大PackUID
            if (objPackUIDDict != null)
            {
                objPackUIDDict[casterKey] = currentPackUID;
            }
            // Debug.LogError($"[ExecuteDamageUpdate] 施法者 - ObjID:{objID}, fromHp:{fromHp}, toHp:{toHp}, maxHp:{maxHp} (PackUID:{currentPackUID})");
        }
        if (maxHp <= 0)
        {
            sliderShield1.value = 0;
            sliderShield2.value = 0;
            return;
        }
        damageSequence = DOTween.Sequence();
        bool IsTianziBoss = battleObject.IsTianziBoss();
        // 护盾动画
        if (fromShield > 0 && !IsTianziBoss)
        {
            float fromShield1Value = Mathf.Min((float)(fromHp + fromShield), (float)maxHp) / (float)maxHp;
            float fromShield2Value = Mathf.Max((float)(fromHp + fromShield - maxHp), 0f) / (float)maxHp;
            sliderShield1.value = fromShield1Value;
            sliderShield2.value = fromShield2Value;
            // 护盾2动画
            if (fromShield2Value > 0)
            {
                float toShield2Value = Mathf.Max((float)(toHp + toShield - maxHp), 0f) / (float)maxHp;
                if (Mathf.Abs(fromShield2Value - toShield2Value) > 0.001f)
                {
                    damageSequence.Append(sliderShield2.DOValue(toShield2Value, 0.2f));
                }
            }
            // 护盾1动画
            if (fromShield1Value > 0)
            {
                float toShield1Value = Mathf.Min((float)(toHp + toShield), (float)maxHp) / (float)maxHp;
                if (Mathf.Abs(fromShield1Value - toShield1Value) > 0.001f)
                {
                    damageSequence.Append(sliderShield1.DOValue(toShield1Value, 0.2f));
                }
            }
        }
        else
        {
            sliderShield1.value = 0f;
            sliderShield2.value = 0f;
        }
        // 血量动画
        float fromHpValue = (float)fromHp / (float)maxHp;
        float toHpValue = (float)toHp / (float)maxHp;
        // sliderHp.value = fromHpValue;
        // sliderSlowHp.value = fromHpValue;
        float diff = Mathf.Abs(toHpValue - fromHpValue);
        float cost = Mathf.Lerp(0, 1f, diff);
        if (Mathf.Abs(fromHpValue - toHpValue) > 0.001f)
        {
            damageSequence.Append(sliderHp.DOValue(toHpValue, cost));
            damageSequence.Join(sliderSlowHp.DOValue(toHpValue, cost * 1.5f));
        }
        damageSequence.onComplete += () =>
        {
            sliderHp.value = toHpValue;
            sliderSlowHp.value = toHpValue;
        };
        damageSequence.Play();
        battleObject.battleField.battleTweenMgr.OnPlayTween(damageSequence);
    }
    
    /// <summary>
    /// 每帧更新
    /// </summary>
    public void Run()
    {
        // 处理血条和伤害队列
        UpdateHpAndDamageQueue();
        // 更新飘字GCD并处理队列
        UpdateTipsGCDAndQueue();
        // 更新所有飘字
        UpdateActiveTips();
        // 处理飘字队列
        ProcessTipsQueue();
    }
    /// <summary>
@@ -225,9 +598,181 @@
        }
    }
    
    #endregion
    private void CompareAndExchangeLargestPackUIDHp(BattleDmgInfo dmgInfo)
    {
        string guid = battleObject.battleField.guid;
        ulong currentPackUID = dmgInfo.battleHurtParam.packUID;
        // 获取或初始化当前战场的数据
        if (!largestPackUID.ContainsKey(guid))
        {
            largestPackUID[guid] = 0ul;
        }
        if (!largestPackUIDAllObjectsToHp.ContainsKey(guid))
        {
            largestPackUIDAllObjectsToHp[guid] = new Dictionary<long, long>();
        }
        if (!largestPackUIDAllObjectsMaxHp.ContainsKey(guid))
        {
            largestPackUIDAllObjectsMaxHp[guid] = new Dictionary<long, long>();
        }
        if (!objectLargestPackUID.ContainsKey(guid))
        {
            objectLargestPackUID[guid] = new Dictionary<long, ulong>();
        }
        ulong currentLargestPackUID = largestPackUID[guid];
        Dictionary<long, long> hpDict = largestPackUIDAllObjectsToHp[guid];
        Dictionary<long, long> maxHpDict = largestPackUIDAllObjectsMaxHp[guid];
        Dictionary<long, ulong> objPackUIDDict = objectLargestPackUID[guid];
        // 如果遇到更大的packUID,更新标记(不清空数据,保留所有证据)
        if (currentPackUID > currentLargestPackUID)
        {
            // Debug.LogError($"[血量记录] 检测到新批次PackUID: {currentPackUID} > {currentLargestPackUID},保留所有历史数据");
            largestPackUID[guid] = currentPackUID;
            currentLargestPackUID = currentPackUID;
        }
        // 记录所有packUID的数据(包括早触发的包),但只采用最大packUID的数据
        // 记录施法者的血量变化
        BattleCastObj battleCastObj = dmgInfo.battleHurtParam.caster;
        if (battleCastObj != null && battleCastObj.casterObj != null)
        {
            long casterID = battleCastObj.casterObj.ObjID;
            // 获取旧血量用于计算变化
            long oldHp = hpDict.ContainsKey(casterID) ? hpDict[casterID] : battleCastObj.fromHp;
            long newHp = battleCastObj.toHp;
            long maxHp = battleCastObj.maxHp;
            long hpChange = newHp - oldHp;
            // 只有当前packUID不小于该对象的最大packUID时才更新记录
            ulong casterLastPackUID = objPackUIDDict.ContainsKey(casterID) ? objPackUIDDict[casterID] : 0ul;
            if (currentPackUID >= casterLastPackUID)
            {
                hpDict[casterID] = newHp;
                maxHpDict[casterID] = maxHp;
                objPackUIDDict[casterID] = currentPackUID;
                // 打印血量变化日志(施法者通常是恢复生命)
                // string casterName = caster.casterObj.teamHero?.heroConfig.Name ?? "未知武将";
                if (hpChange != 0)
                {
                    // string changeType = hpChange > 0 ? "恢复" : "损失";
                    // Debug.LogError($"[血量变化] {casterName}(ID:{casterID}) {changeType} {Math.Abs(hpChange)} 生命,血量从 {oldHp}/{maxHp} 变为 {newHp}/{maxHp} (PackUID:{currentPackUID})");
                }
            }
            else
            {
                // Debug.LogWarning($"[血量记录] 忽略旧包数据 - 施法者{casterID}, 当前PackUID:{currentPackUID} < 对象最大PackUID:{casterLastPackUID}");
            }
        }
        // 记录受击者的血量变化
        BattleHurtObj battleHurtObj = dmgInfo.battleHurtParam.hurter;
        if (battleHurtObj != null && battleHurtObj.hurtObj != null)
        {
            BattleObject hurter = battleHurtObj.hurtObj;
            long hurterID = hurter.ObjID;
            // 获取旧血量用于计算伤害
            long oldHp = hpDict.ContainsKey(hurterID) ? hpDict[hurterID] : battleHurtObj.fromHp;
            long newHp = battleHurtObj.toHp;
            long maxHp = battleHurtObj.maxHp;
            long damage = oldHp - newHp;
            // 只有当前packUID不小于该对象的最大packUID时才更新记录
            ulong hurterLastPackUID = objPackUIDDict.ContainsKey(hurterID) ? objPackUIDDict[hurterID] : 0ul;
            if (currentPackUID >= hurterLastPackUID)
            {
                hpDict[hurterID] = newHp;
                maxHpDict[hurterID] = maxHp;
                objPackUIDDict[hurterID] = currentPackUID;
                // 打印血量变化日志
                // string hurterName = hurter.hurtObj.teamHero?.heroConfig.Name ?? "未知武将";
                if (damage != 0)
                {
                    // Debug.LogError($"[血量变化] {hurterName}(ID:{hurterID}) 受到 {damage} 伤害,血量从 {oldHp}/{maxHp} 变为 {newHp}/{maxHp} (PackUID:{currentPackUID})");
                }
            }
            else
            {
                // Debug.LogWarning($"[血量记录] 忽略旧包数据 - 受击者{hurterID}, 当前PackUID:{currentPackUID} < 对象最大PackUID:{hurterLastPackUID}");
            }
        }
    }
    #region 私有方法 - 飘字处理
    /// <summary>
    /// 处理血条和伤害更新队列
    /// </summary>
    private void UpdateHpAndDamageQueue()
    {
        // 优先处理UpdateDamage
        if (damageUpdateQueue.Count > 0)
        {
            BattleDmgInfo dmgInfo = damageUpdateQueue.Dequeue();
            CompareAndExchangeLargestPackUIDHp(dmgInfo);
            ExecuteDamageUpdate(dmgInfo);
            return;
        }
    }
    /// <summary>
    /// 更新飘字GCD并处理队列
    /// </summary>
    private void UpdateTipsGCDAndQueue()
    {
        // 更新GCD计时器
        if (tipsGCDTimer > 0f)
        {
            float speedRatio = GetCurrentSpeedRatio();
            float deltaTime = 1f / (float)BattleConst.skillMotionFps * speedRatio;
            tipsGCDTimer -= deltaTime;
            if (tipsGCDTimer < 0f)
            {
                tipsGCDTimer = 0f;
            }
        }
        // 如果GCD结束且有待处理的飘字,弹出一个
        if (tipsGCDTimer <= 0f && messages.Count > 0)
        {
            TipsInfo tipsInfo = messages[0];
            messages.RemoveAt(0);
            PopUpTipsDirectly(tipsInfo);
            // 重置GCD
            ResetTipsGCD();
        }
    }
    /// <summary>
    /// 重置飘字GCD计时器
    /// </summary>
    private void ResetTipsGCD()
    {
        float speedRatio = GetCurrentSpeedRatio();
        float frameTime = 1f / (float)BattleConst.skillMotionFps;
        tipsGCDTimer = frameTime * TIPS_GCD_FRAMES / speedRatio;
    }
    /// <summary>
    /// 获取当前速度倍率
    /// </summary>
    private float GetCurrentSpeedRatio()
    {
        // 回退到战场速度
        if (battleObject != null && battleObject.battleField != null)
        {
            return battleObject.battleField.speedRatio;
        }
        return 1f;
    }
    
    /// <summary>
    /// 立即弹出飘字
@@ -236,24 +781,23 @@
    {
        // 创建飘字实例
        BattleTips tips = CreateTipsInstance(tipsInfo);
        // 配置飘字
        ConfigureTips(tips, tipsInfo);
        // 设置位置(如果不跟随)
        if (!tipsInfo.followCharacter)
        {
            SetNonFollowPosition(tips);
        }
        // 设置参数并显示
        tips.SetRatio(battleObject.battleField.speedRatio, tipsInfo.scaleRatio);
        tips.SetText(tipsInfo.message, tipsInfo.useArtText, false); // 移除 textColor 参数
        tips.ShowBackground(tipsInfo.showBackground);
        // 注册完成回调
        tips.OnFinish = () => RemoveTips(tips);
        tips.SetRatio(battleObject.battleField.speedRatio, tipsInfo.scaleRatio);
        tips.ShowBackground(tipsInfo.showBackground);
        tips.SetText(tipsInfo.message, tipsInfo.useArtText, false);
        // 添加到列表
        tipsList.Add(tips);
    }
@@ -265,7 +809,7 @@
    {
        Transform parent = tipsInfo.followCharacter 
            ? transform 
            : battleObject.battleField.battleRootNode.transform;
            : battleObject.battleField.battleRootNode.notFollowTipsAdjuster.transform;
            
        GameObject go = GameObject.Instantiate(textTips.gameObject, parent);
        return go.GetComponent<BattleTips>();
@@ -276,7 +820,7 @@
    /// </summary>
    private void ConfigureTips(BattleTips tips, TipsInfo tipsInfo)
    {
        FloatingConfig targetConfig = tipsInfo.followCharacter
        FloatingConfig targetConfig = tipsInfo.isRage ? rageFloatingConfig : tipsInfo.followCharacter
            ? followFloatingConfig 
            : noFollowFloatingConfig;
        
@@ -327,6 +871,7 @@
    private void RemoveTips(BattleTips tips)
    {
        tipsList.Remove(tips);
        tips.controller = null;
        GameObject.DestroyImmediate(tips.gameObject);
    }
    
@@ -337,25 +882,13 @@
    {
        for (int i = tipsList.Count - 1; i >= 0; i--)
        {
            if (tipsList[i].gameObject == null)
            {
                var instanceid = tipsList[i].gameObject.GetInstanceID();
                tipsList.RemoveAt(i);
                continue;
            }
            tipsList[i].Run();
        }
    }
    /// <summary>
    /// 处理飘字队列
    /// </summary>
    private void ProcessTipsQueue()
    {
        timer += GetDeltaTime();
        if (messages.Count > 0 && timer >= PopUpInterval)
        {
            TipsInfo tipsInfo = messages[0];
            messages.RemoveAt(0);
            PopUpTipsDirectly(tipsInfo);
            timer = 0f;
        }
    }
    
@@ -366,23 +899,18 @@
    {
        messages.Clear();
        
        foreach (var tip in tipsList)
        for (int i = tipsList.Count - 1; i >= 0; i--)
        {
            tip.OnFinish = null;
            GameObject.DestroyImmediate(tip.gameObject);
            RemoveTips(tipsList[i]);
        }
        
        tipsList.Clear();
    }
    
    #endregion
    #region 私有方法 - 辅助方法
    /// <summary>
    /// 停止并清理Tween
    /// </summary>
    private void KillTween(ref Tween tween)
    private void KillTween<T>(ref T tween) where T : Tween
    {
        if (tween != null && battleObject != null)
        {
@@ -406,6 +934,26 @@
    {
        // TODO: 显示buff描述/当前身上所有buff
    }
    #endregion
    /// <summary>
    /// 清理指定战场的静态PackUID和血量记录
    /// </summary>
    public static void ClearStaticBattleData(string guid)
    {
        largestPackUID.Remove(guid);
        largestPackUIDAllObjectsToHp.Remove(guid);
        largestPackUIDAllObjectsMaxHp.Remove(guid);
        objectLargestPackUID.Remove(guid);
    }
    public void HaveRest()
    {
        CleanupTips();
        SetActive(false);
        //  关掉所有的tween
        KillTween(ref hpTween);
        KillTween(ref xpTween);
        KillTween(ref damageSequence);
    }
}