using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using DG.Tweening; using DG.Tweening.Core; using System.Linq; public static class BattleUtility { // 其他通用的战斗工具方法可以放在这里 public static void MarkStartAndEnd(RectTransform startNode, RectTransform endNode) { // 运行时才执行 if (!Application.isPlaying) { Debug.LogWarning("请在运行时使用该功能!"); return; } var battleField = BattleManager.Instance.storyBattleField; if (battleField == null) { Debug.LogError("BattleManager.storyBattleField 未初始化!"); return; } BattleWin battleWin = UIManager.Instance.GetUI(); RectTransform canvasRect = battleWin.transform as RectTransform; CreateMarker(canvasRect, startNode, "StartMarker"); CreateMarker(canvasRect, endNode, "EndMarker"); } private static void CreateMarker(RectTransform canvasRect, RectTransform targetNode, string markerName) { // 获取目标节点的世界坐标(中心点) Vector3 worldPos = targetNode.TransformPoint(targetNode.rect.center); // 转换到Canvas本地坐标 Vector2 localPoint; RectTransformUtility.ScreenPointToLocalPointInRectangle( canvasRect, RectTransformUtility.WorldToScreenPoint(null, worldPos), null, out localPoint); // 创建RawImage GameObject marker = new GameObject(markerName, typeof(RawImage)); GameObject.Destroy(marker, 5f); marker.transform.SetParent(canvasRect, false); var rawImage = marker.GetComponent(); rawImage.color = Color.white; rawImage.rectTransform.sizeDelta = new Vector2(100, 100); rawImage.rectTransform.anchoredPosition = localPoint; } public static TweenerCore MoveToTarget( RectTransform transform, RectTransform target, Vector2 offset, Action onComplete = null, float speed = 500f) { // 获取目标节点的世界坐标(锚点位置) Vector3 worldPos = target.position; // 如果需要加 offset,需考虑 scale Vector3 offsetWorld = target.TransformVector(offset); worldPos += offsetWorld; RectTransform canvasRect = transform.parent as RectTransform; // 转换到Canvas本地坐标 Vector2 localPoint; RectTransformUtility.ScreenPointToLocalPointInRectangle( canvasRect, RectTransformUtility.WorldToScreenPoint(null, worldPos), null, out localPoint); float distance = Vector2.Distance(transform.anchoredPosition, localPoint); float duration = distance / speed; var tween = transform.DOAnchorPos(localPoint, duration).SetEase(Ease.Linear); tween.onComplete += () => { onComplete?.Invoke(); }; return tween; } public static string DisplayDamageNum(long num, int attackType) { var config = DamageNumConfig.Get(attackType); var basePowerStr = UIHelper.ReplaceLargeArtNum(num); var result = string.Empty; for (int i = 0; i < basePowerStr.Length; i++) { var numChar = (char)GetDamageNumKey(config, basePowerStr[i]); if (numChar > 0) { result += numChar; } } return result; } public static string DisplayDamageNum(BattleDmg damage) { var config = DamageNumConfig.Get(damage.attackType); string result = string.Empty; // 如果是闪避或免疫 则只显示对应文字,不显示数字 if (damage.IsType(DamageType.Dodge) || damage.IsType(DamageType.Immune)) { result += (char)config.prefix; } else { result = ConvertToArtFont(config, damage.damage); } return result; } public static string ConvertToArtFont(DamageNumConfig config, float _num) { var stringBuild = new System.Text.StringBuilder(); if (0 != config.plus) stringBuild.Append((char)config.plus); if (0 != config.prefix) stringBuild.Append((char)config.prefix); var chars = UIHelper.ReplaceLargeArtNum(_num); for (var i = 0; i < chars.Length; i++) { int numChar = GetDamageNumKey(config, (int)chars[i]); if (numChar > 0) { stringBuild.Append((char)numChar); } } return stringBuild.ToString(); } public static int GetMainTargetPositionNum(SkillBase skillBase, BattleObject caster, List targetList, SkillConfig skillConfig) { int returnIndex = 0; // 根据敌方血量阵营 存活人数来选择 BattleCamp battleCamp = skillConfig.TagFriendly != 0 ? caster.Camp : caster.GetEnemyCamp(); List targetObjList = caster.battleField.battleObjMgr.GetBattleObjList(battleCamp); // 瞄准的目标范围,如果目标个数为0则为范围内全部 // 0 全部范围 // 1 对位,默认只选1个 // 2 前排 // 3 后排 // 4 纵排,按对位规则选择纵排 // 5 自己,默认只选自己 switch (skillConfig.TagAim) { case 0: // 0 全部范围: // 若TagCount目标个数为0或6,根据TagFriendly敌我配置,代表作用于敌方全体或我方全体,此时主目标为敌我站位中的2号位置 // 若TagCount目标个数为1~5个,根据TagFriendly敌我+TagAffect细分目标配置,代表随机作用于敌方或我方x个武将,第一个为主目标 if (skillConfig.TagCount == 0 || skillConfig.TagCount == 6) { returnIndex = 1; } else { uint objId = targetList[0].ObjID; BattleObject target = caster.battleField.battleObjMgr.GetBattleObject((int)objId); return target.teamHero.positionNum; } break; case 1: // 1 对位: // 默认只选1个,对位规则为A1优先打B1,A2优先打B2,A3优先打B3,对位目标死亡时,优先前排,比如B2已经死亡,那么A2将优先打B if (targetList.Count > 0) { BattleObject battleObject = caster.battleField.battleObjMgr.GetBattleObject((int)targetList[0].ObjID); if (battleObject != null) { returnIndex = battleObject.teamHero.positionNum; } else { Debug.LogError("GetMainTargetPositionNum 找不到目标 ObjId : " + targetList[0].ObjID); returnIndex = 0; } } else { Debug.LogError("targetList 目标列表为空"); returnIndex = 0; } break; case 2: // 1、2、3号位为前排,默认2号位置为主目标,当1、2、3号位置角色全部死亡,前排将替换成后排,5号位置变更为主目标, // 若配置TagAffect细分目标,且人数小于3,则所有被选择目标均为主目标(施法位置会用客户端配置) // (即前排默认2号位或5号位规则无效,实际作用多少人就是多少个主目标) (YL : TagAffect>0 && TagAffect != 3就是全体都是主目标 后排一样 ) if (skillConfig.TagAffect != 0 || skillConfig.TagCount < 3) { uint objId = targetList[0].ObjID; BattleObject target = caster.battleField.battleObjMgr.GetBattleObject((int)objId); returnIndex = target.teamHero.positionNum; } else { // 看看对面前排是否都活着 List front = new List(from bo in targetObjList where !bo.IsDead() && bo.teamHero.positionNum < 3 select bo); if (front.Count > 0) { returnIndex = 1; } else { returnIndex = 4; } } break; case 3: if (skillConfig.TagAffect != 0 || skillConfig.TagCount < 3) { uint objId = targetList[0].ObjID; BattleObject target = caster.battleField.battleObjMgr.GetBattleObject((int)objId); returnIndex = target.teamHero.positionNum; } else { // 看看对面后排是否都活着 List back = new List(from bo in targetObjList where !bo.IsDead() && bo.teamHero.positionNum >= 3 select bo); if (back.Count > 0) { returnIndex = 4; } else { returnIndex = 1; } } break; // 4 纵排,按对位规则选择纵排 case 4: returnIndex = int.MaxValue; for (int i = 0; i < targetList.Count; i++) { var hurt = targetList[i]; BattleObject target = caster.battleField.battleObjMgr.GetBattleObject((int)hurt.ObjID); if (target == null) { Debug.LogError("GetMainTargetPositionNum 找不到目标 ObjId : " + hurt.ObjID); continue; } else { returnIndex = Mathf.Min(returnIndex, target.teamHero.positionNum); } } break; // 5 自己,默认只选自己 case 5: returnIndex = caster.teamHero.positionNum; break; case 6: // 跟随主技能的目标 var fromSkill = skillBase.fromSkill; returnIndex = GetMainTargetPositionNum(fromSkill, fromSkill.caster, fromSkill.tagUseSkillAttack.HurtList.ToList(), fromSkill.skillConfig); break; default: Debug.LogError("暂时不支持其他的方式选择主目标 有需求请联系策划 技能id:" + skillConfig.SkillID + " TagAim " + skillConfig.TagAim); returnIndex = 0; break; } return returnIndex; } public static int GetDamageNumKey(DamageNumConfig config, int _num) { if (_num == 46) return config.nums[10]; // '.' else if (_num == 107) return config.nums[11]; // 'k' else if (_num == 109) return config.nums[12]; // 'm' else if (_num == 98) return config.nums[13]; // 'b' else if (_num == 116) return config.nums[14]; // 't' int targetNum = _num - 48; if (targetNum >= config.nums.Length || targetNum < 0) { Debug.LogError("damage config " + config.TypeID + " _num is " + _num + " out of range"); return _num; } return config.nums[_num - 48]; } public static bool IsHealing(HB427_tagSCUseSkill.tagSCUseSkillHurt hurt) { return ((hurt.AttackTypes & (int)ServerDamageType.Recovery) != 0 || (hurt.AttackTypes & (int)ServerDamageType.DamageReverse) != 0) && (hurt.AttackTypes & (int)ServerDamageType.Damage) == 0 && (hurt.AttackTypes & (int)ServerDamageType.Realdamage) == 0 && (hurt.AttackTypes & (int)ServerDamageType.SuckHpReverse) == 0 && (hurt.AttackTypes & (int)ServerDamageType.SelfHarm) == 0; } public static long GetSuckHp(HB427_tagSCUseSkill hB427_TagSCUseSkill) { long totalSuckHp = 0; for (int i = 0; i < hB427_TagSCUseSkill.HurtList.Length; i++) { var hurt = hB427_TagSCUseSkill.HurtList[i]; totalSuckHp += hurt.SuckHP; for (int k = 0; k < hurt.HurtListEx.Length; k++) { var hurtEx = hurt.HurtListEx[k]; totalSuckHp += hurtEx.SuckHP; } } for (int i = 0; i < hB427_TagSCUseSkill.HurtListEx.Length; i++) { var hurt = hB427_TagSCUseSkill.HurtListEx[i]; totalSuckHp += hurt.SuckHP; } return totalSuckHp; } public static BattleHurtParam CalcBattleHurtParam(SkillBase skillBase, int hitIndex, BattleObject target, HB427_tagSCUseSkill.tagSCUseSkillHurt hurt, BattleDrops battleDrops, BattleDeadPack deadPack) { long suckHp = GetSuckHp(skillBase.tagUseSkillAttack); SkillConfig skillConfig = skillBase.skillConfig; long totalDamage = GeneralDefine.GetFactValue(hurt.HurtHP, hurt.HurtHPEx); long totalReflectHp = hurt.BounceHP; // 计算伤害分段 long currentHitDamage = 0; List damageList = DivideDamageToList(skillConfig.DamageDivide, hitIndex, totalDamage, ref currentHitDamage); // 计算吸血分段 long currentHitSuckHp = 0; List suckHpList = DivideDamageToList(skillConfig.DamageDivide, hitIndex, suckHp, ref currentHitSuckHp); // 计算反伤分段 long currentHitReflectHp = 0; List reflectHpList = DivideDamageToList(skillConfig.DamageDivide, hitIndex, totalReflectHp, ref currentHitReflectHp); // 创建目标受伤对象 BattleHurtObj hurter = CreateHurter(target, damageList, hurt, hitIndex, skillConfig, currentHitDamage); // 创建施法者对象 BattleCastObj caster = CreateCaster(skillBase, suckHpList, reflectHpList, currentHitSuckHp, currentHitReflectHp); // 组装BattleHurtParam BattleHurtParam battleHurtParam = new BattleHurtParam(); battleHurtParam.caster = caster; battleHurtParam.hurter = hurter; battleHurtParam.battleDrops = battleDrops; battleHurtParam.hurt = hurt; battleHurtParam.hB427_TagSCUseSkill = skillBase.tagUseSkillAttack; battleHurtParam.hitIndex = hitIndex; battleHurtParam.deadPack = deadPack; battleHurtParam.skillConfig = skillConfig; battleHurtParam.packUID = skillBase.tagUseSkillAttack.packUID; return battleHurtParam; } public static BattleHurtObj CreateHurter(BattleObject target, List damageList, HB427_tagSCUseSkill.tagSCUseSkillHurt hurt, int hitIndex, SkillConfig skillConfig, long currentHitDamage) { BattleHurtObj hurter = new BattleHurtObj(); hurter.hurtObj = target; hurter.damageList = damageList; hurter.fromHp = target.teamHero.curHp; hurter.fromShieldValue = target.buffMgr.GetShieldValue(); // 判断是否是最后一击 bool isLastHit = hitIndex >= skillConfig.DamageDivide.Length - 1; // 判断是治疗还是伤害 bool isHealing = IsHealing(hurt); // 计算目标血量变化 if (isLastHit) { // 最后一击:使用服务器下发的最终血量 hurter.toHp = GeneralDefine.GetFactValue(hurt.CurHP, hurt.CurHPEx); } else { // 非最后一击:客户端计算中间血量 long maxHp = target.teamHero.maxHp; if (isHealing) { // 治疗逻辑:直接加血 hurter.toHp = Math.Min(maxHp, hurter.fromHp + currentHitDamage); } else { // 伤害逻辑:先扣护盾,护盾不足再扣血 if (hurter.fromShieldValue >= currentHitDamage) { hurter.toHp = hurter.fromHp; target.teamHero.curHp = hurter.toHp; // 保持一致性 } else { long remainingDamage = currentHitDamage - hurter.fromShieldValue; hurter.toHp = Math.Max(0, hurter.fromHp - remainingDamage); target.teamHero.curHp = hurter.toHp; // 保持一致性 } } } // 计算护盾变化 if (isHealing) { hurter.toShieldValue = hurter.fromShieldValue; } else { if (hurter.fromShieldValue >= currentHitDamage) { hurter.toShieldValue = hurter.fromShieldValue - currentHitDamage; } else { hurter.toShieldValue = 0; } } return hurter; } public static BattleCastObj CreateCaster(SkillBase skillBase, List suckHpList, List reflectHpList, long currentHitSuckHp, long currentHitReflectHp) { BattleCastObj caster = new BattleCastObj(); caster.casterObj = skillBase.caster; caster.suckHpList = suckHpList; caster.reflectHpList = reflectHpList; // 获取施法者当前状态 long casterFromHp = skillBase.caster.teamHero.curHp; long casterMaxHp = skillBase.caster.teamHero.maxHp; long casterFromShield = skillBase.caster.buffMgr.GetShieldValue(); caster.fromHp = casterFromHp; caster.fromShieldValue = casterFromShield; // 计算施法者血量变化(吸血和反伤) long casterToHp = casterFromHp; long casterToShield = casterFromShield; // 处理吸血 if (currentHitSuckHp > 0) { casterToHp = Math.Min(casterMaxHp, casterToHp + currentHitSuckHp); } // 处理反伤(施法者受到伤害) if (currentHitReflectHp > 0) { if (casterToShield >= currentHitReflectHp) { // 施法者护盾足够,只扣护盾 casterToShield -= currentHitReflectHp; } else { // 施法者护盾不足,先扣完护盾,剩余扣血 long remainingReflect = currentHitReflectHp - casterToShield; casterToShield = 0; casterToHp = Math.Max(0, casterToHp - remainingReflect); } } caster.toHp = casterToHp; caster.toShieldValue = casterToShield; skillBase.caster.teamHero.curHp = caster.toHp; // 保持一致性 return caster; } /// /// 将整个技能的总伤害按命中次数和分段配置分配 /// /// 整个技能的所有命中分段配置 /// 当前是第几击(从0开始) /// 整个技能的总伤害 /// 这一击内每一段的伤害值列表 public static List DivideDamageToList(int[][] damageDivideList, int hitIndex, long totalDamage, ref long currentHitDamage) { if (totalDamage <= 0) { return new List{}; } if (damageDivideList == null || damageDivideList.Length == 0) { Debug.LogError("damageDivideList 为空或长度为0"); return new List { totalDamage }; } if (hitIndex < 0 || hitIndex >= damageDivideList.Length) { Debug.LogError($"hitIndex={hitIndex} 超出范围, damageDivideList.Length={damageDivideList.Length}"); return new List { totalDamage }; } int[] currentHitDivide = damageDivideList[hitIndex]; if (currentHitDivide == null || currentHitDivide.Length == 0) { Debug.LogError($"damageDivide[{hitIndex}] 为空或长度为0"); return new List { totalDamage }; } // ============ 第一步: 计算每一击应该造成的伤害 ============ // 先计算所有击的总权重 int totalWeight = 0; for (int i = 0; i < damageDivideList.Length; i++) { if (damageDivideList[i] != null && damageDivideList[i].Length > 0) { // 每一击的权重是其所有分段之和 for (int j = 0; j < damageDivideList[i].Length; j++) { totalWeight += damageDivideList[i][j]; } } } if (totalWeight == 0) { Debug.LogError("totalWeight 为 0"); return new List { totalDamage }; } // 计算当前这一击的权重 int currentHitWeight = 0; for (int i = 0; i < currentHitDivide.Length; i++) { currentHitWeight += currentHitDivide[i]; } // 计算当前这一击应该造成的总伤害 long currentHitTotalDamage; bool isLastHit = hitIndex >= damageDivideList.Length - 1; if (isLastHit) { // 最后一击: 计算前面所有击已经造成的伤害,剩余的全部给最后一击 long previousHitsDamage = 0; for (int i = 0; i < hitIndex; i++) { if (damageDivideList[i] != null) { int hitWeight = 0; for (int j = 0; j < damageDivideList[i].Length; j++) { hitWeight += damageDivideList[i][j]; } previousHitsDamage += (long)((float)totalDamage * (float)hitWeight / (float)totalWeight); } } currentHitTotalDamage = totalDamage - previousHitsDamage; } else { // 非最后一击: 按权重计算 currentHitTotalDamage = (long)((float)totalDamage * (float)currentHitWeight / (float)totalWeight); } currentHitDamage = currentHitTotalDamage; // ============ 第二步: 将当前这一击的伤害分配到各分段 ============ List fixedDamageList = new List(); long accumulatedDamage = 0; for (int i = 0; i < currentHitDivide.Length; i++) { long damage; // 当前击的最后一段进行误差补偿 if (i == currentHitDivide.Length - 1) { damage = currentHitTotalDamage - accumulatedDamage; } else { // 按当前击的权重分配 damage = (long)((float)currentHitTotalDamage * (float)currentHitDivide[i] / (float)currentHitWeight); accumulatedDamage += damage; } fixedDamageList.Add(damage); } return fixedDamageList; } /// /// 保证所有分配项加起来等于totalDamage,避免因整除导致的误差 /// public static List DivideDamageToList(int[] damageDivide, long totalDamage) { if (damageDivide == null || damageDivide.Length == 0) { Debug.LogError("damageDivide 为空或长度为0"); return new List { totalDamage }; } List fixedDamageList = new List(); long accumulatedDamage = 0; // 累计已分配的伤害 for (int i = 0; i < damageDivide.Length; i++) { long damage; // 最后一次分配:用总伤害减去已分配的伤害,确保总和精确 if (i == damageDivide.Length - 1) { damage = totalDamage - accumulatedDamage; } else { // 计算当前分段伤害(向下取整) damage = (long)((float)totalDamage * (float)damageDivide[i] / 10000f); accumulatedDamage += damage; } fixedDamageList.Add(damage); } return fixedDamageList; } public static HB419_tagSCObjHPRefresh FindObjHPRefreshPack(List packList) { for (int i = 0; i < packList.Count; i++) { var pack = packList[i]; if (pack is HB419_tagSCObjHPRefresh hpRefreshPack) { return hpRefreshPack; } else if (pack is CustomHB426CombinePack) { break; } } return null; } public static List FindDeadPack(List packList) { List deadPacks = new List(); for (int i = 0; i < packList.Count; i++) { var pack = packList[i]; // 寻找死亡包 找到死亡包之后要找掉落包 不能超过技能包 if (pack is HB422_tagMCTurnFightObjDead deadPack) { deadPacks.Add(new BattleDeadPack { deadPack = deadPack }); } else if (pack is CustomHB426CombinePack combinePack) { if (deadPacks.Count > 0) { HB427_tagSCUseSkill hB427_TagSCUseSkill = combinePack.GetMainHB427SkillPack(); if (null == hB427_TagSCUseSkill) { break; } else { var skillID = hB427_TagSCUseSkill.SkillID; var skillConfig = SkillConfig.Get((int)skillID); if ((SkillTriggerTiming)skillConfig.ClientTriggerTiming == SkillTriggerTiming.Death) { for (int j = 0 ; j < deadPacks.Count; j ++) { var parentPack = deadPacks[j]; if (parentPack.IsOwnSkill(hB427_TagSCUseSkill)) { parentPack.SetDeathSkill(combinePack); break; } } } else { break; } } } else { break; } } } // Debug.LogError("find dead pack " + deadPacks.Count); return deadPacks; } public static List FindRebornPack(List packList) { List rebornPackList = new List(); for (int i = 0; i < packList.Count; i++) { var pack = packList[i]; // 寻找死亡包 找到死亡包之后要找掉落包 不能超过技能包 if (pack is HB423_tagMCTurnFightObjReborn rebornPack) { rebornPackList.Add(rebornPack); } else if (pack is CustomHB426CombinePack) { break; } } return rebornPackList; } // ============================================================ // 文件 2: BattleUtility.cs // 添加血量一致性验证函数 // ============================================================ /// /// 验证目标血量是否与服务器包一致(仅在最后一击时调用) /// public static void ValidateHpConsistency(BattleHurtParam hurtParam, string context) { #if UNITY_EDITOR BattleHurtObj hurter = hurtParam.hurter; HB427_tagSCUseSkill.tagSCUseSkillHurt hurt = hurtParam.hurt; // 获取服务器下发的最终血量 long serverFinalHp = GeneralDefine.GetFactValue(hurt.CurHP, hurt.CurHPEx); // 获取客户端计算的最终血量 long clientFinalHp = hurter.toHp; // 验证是否一致 bool isConsistent = (serverFinalHp == clientFinalHp); string logColor = isConsistent ? "" : ""; string resultStr = isConsistent ? "✓ 一致" : "✗ 不一致"; BattleDebug.LogError( $"{logColor}========== 血量一致性验证 [{context}] {resultStr} ==========\n" + $"目标: {hurter.hurtObj.teamHero.name} (ObjID:{hurter.hurtObj.ObjID})\n" + $"击数: 第{hurtParam.hitIndex + 1}击 (最后一击)\n" + $"服务器最终血量: {serverFinalHp}\n" + $"客户端计算血量: {clientFinalHp}\n" + $"血量差异: {clientFinalHp - serverFinalHp}\n" + $"\n" + $"---- 血量变化过程 ----\n" + $"初始血量: {hurter.fromHp}\n" + $"本次伤害/治疗: {(hurter.damageList != null ? hurter.damageList.Sum() : 0)}\n" + $"最终血量: {clientFinalHp}\n" + $"最大血量: {hurter.hurtObj.teamHero.maxHp}\n" + $"\n" + $"---- 护盾变化过程 ----\n" + $"初始护盾: {hurter.fromShieldValue}\n" + $"最终护盾: {hurter.toShieldValue}\n" + $"护盾变化: {hurter.toShieldValue - hurter.fromShieldValue}\n" ); // 如果不一致,额外输出警告 if (!isConsistent) { Debug.LogWarning( $"[血量验证失败] 目标:{hurter.hurtObj.teamHero.name} " + $"服务器:{serverFinalHp} vs 客户端:{clientFinalHp} " + $"差异:{clientFinalHp - serverFinalHp}" ); } #endif } /// /// 验证施法者血量是否正确(仅在最后一击时调用) /// public static void ValidateHpConsistencyForCaster(BattleHurtParam hurtParam, string context) { #if UNITY_EDITOR BattleCastObj caster = hurtParam.caster; // 施法者没有服务器下发的最终血量,只能验证计算逻辑是否正确 long clientFinalHp = caster.toHp; long calculatedHp = caster.fromHp; // 计算预期的血量变化 long totalSuckHp = caster.suckHpList != null ? caster.suckHpList.Sum() : 0; long totalReflectHp = caster.reflectHpList != null ? caster.reflectHpList.Sum() : 0; // 模拟计算过程 long expectedHp = calculatedHp; long expectedShield = caster.fromShieldValue; // 应用吸血 if (totalSuckHp > 0) { expectedHp = Math.Min(caster.casterObj.teamHero.maxHp, expectedHp + totalSuckHp); } // 应用反伤 if (totalReflectHp > 0) { if (expectedShield >= totalReflectHp) { expectedShield -= totalReflectHp; } else { long remainingReflect = totalReflectHp - expectedShield; expectedShield = 0; expectedHp = Math.Max(0, expectedHp - remainingReflect); } } bool isConsistent = (expectedHp == clientFinalHp); string logColor = isConsistent ? "" : ""; string resultStr = isConsistent ? "✓ 计算正确" : "✗ 计算错误"; BattleDebug.LogError( $"{logColor}========== 施法者血量验证 [{context}] {resultStr} ==========\n" + $"施法者: {caster.casterObj.teamHero.name} (ObjID:{caster.casterObj.ObjID})\n" + $"击数: 第{hurtParam.hitIndex + 1}击 (最后一击)\n" + $"预期最终血量: {expectedHp}\n" + $"实际最终血量: {clientFinalHp}\n" + $"血量差异: {clientFinalHp - expectedHp}\n" + $"\n" + $"---- 血量变化过程 ----\n" + $"初始血量: {caster.fromHp}\n" + $"吸血总量: {totalSuckHp}\n" + $"反伤总量: {totalReflectHp}\n" + $"最终血量: {clientFinalHp}\n" + $"最大血量: {caster.casterObj.teamHero.maxHp}\n" + $"\n" + $"---- 护盾变化过程 ----\n" + $"初始护盾: {caster.fromShieldValue}\n" + $"预期护盾: {expectedShield}\n" + $"实际护盾: {caster.toShieldValue}\n" + $"护盾差异: {caster.toShieldValue - expectedShield}\n" ); // 如果不一致,额外输出警告 if (!isConsistent) { Debug.LogWarning( $"[施法者血量计算错误] {caster.casterObj.teamHero.name} " + $"预期:{expectedHp} vs 实际:{clientFinalHp} " + $"差异:{clientFinalHp - expectedHp}" ); } #endif } }