yyl
2026-04-24 64d526e0f5b9f49fe5bd6dcf043f5071d996e7ad
125 战斗 修复步练师的被动bug
5个文件已修改
119 ■■■■■ 已修改文件
Main/System/Battle/BattleObject/BattleObject.cs 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleObject/HeroBattleObject.cs 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/Motion/MotionBase.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/Skill/SkillBase.Cast.cs 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/Skill/SkillBase.Finish.cs 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleObject/BattleObject.cs
@@ -41,6 +41,10 @@
        battleField = _battleField;
    }
    //  SkillBase.MoveToTarget 发起的前冲 / 回位 tween 句柄(由 SkillBase 维护)。
    //  CanCastSkillAnimation 只判断这一个句柄,避免误伤 caster 身上其它业务 tween。
    public Tween activeMoveTween;
    // ============ 抽象访问方法(子类返回各自的Team类型信息) ============
    
    public abstract int GetPositionNum();
Main/System/Battle/BattleObject/HeroBattleObject.cs
@@ -198,6 +198,15 @@
    
    public override bool CanCastSkillAnimation(SkillSkinConfig skillSkinConfig)
    {
        //  除了等 Spine 技能动画锁(playingSkillWithAnim),还必须等 caster 自身由 SkillBase.MoveToTarget
        //  发起的前冲 / 回位 tween 跑完。否则上一个技能的回位 tween 还在半路,下一个技能(尤其是被动
        //  BattleType=5)会在当前位置再发起前冲,旧回位 tween 和新前冲 tween 同时写 anchoredPosition,
        //  视觉表现是"没回位又突然冲出去攻击"。这里只判定 SkillBase 自己记录的移动 tween 句柄,不误伤
        //  挂在 heroRectTrans 上的其它业务 tween。
        if (activeMoveTween != null && activeMoveTween.IsActive() && !activeMoveTween.IsComplete())
        {
            return false;
        }
        return motionBase.CanCastSkill(skillSkinConfig);
    }
    
Main/System/Battle/Motion/MotionBase.cs
@@ -45,6 +45,13 @@
    private bool playingSkill = false;
    private bool _playingSkillWithAnim = false;
#if UNITY_EDITOR
    //  [卡死诊断] 记录是谁把 playingSkillWithAnim 锁为 true、在哪一帧,供 SkillBase.ReportStuckIfNeeded 使用。
    //  不做 Run() 内 watchdog(避免刷屏);只在 SkillBase 自身发现卡死时一次性把这些信息打出来。
    private int _pswaSetTrueFrame = -1;
    private int _pswaSetTrueSkillId = 0;
    private string _pswaSetTrueStack = null;
#endif
    private bool playingSkillWithAnim
    {
        get => _playingSkillWithAnim;
@@ -58,6 +65,19 @@
                // 每次变更打印栈,定位到底是谁把 playingSkillWithAnim 设 true 了、谁清/不清
                BattleDebug.LogError($"[MotionBase owner={owner} hash={GetHashCode()}] playingSkillWithAnim {_playingSkillWithAnim} -> {value}\n"
                    + UnityEngine.StackTraceUtility.ExtractStackTrace());
            }
            //  [卡死诊断] 维护锁的持有者信息(上一轮的字段被回滚了,这里是最小必要集合)
            if (value && !_playingSkillWithAnim)
            {
                _pswaSetTrueFrame = UnityEngine.Time.frameCount;
                _pswaSetTrueStack = UnityEngine.StackTraceUtility.ExtractStackTrace();
                // skillId 由 ExecuteSkillAnim 紧接着赋值(setter 这里拿不到)
            }
            else if (!value && _playingSkillWithAnim)
            {
                _pswaSetTrueFrame = -1;
                _pswaSetTrueSkillId = 0;
                _pswaSetTrueStack = null;
            }
#endif
            _playingSkillWithAnim = value;
@@ -335,6 +355,10 @@
            activeSkillTracks[trackIndex] = skillTrack;
            playingSkillWithAnim = true;
#if UNITY_EDITOR
            //  [卡死诊断] setter 里拿不到 skillId,这里补上,供 SkillBase.ReportStuckIfNeeded dump
            _pswaSetTrueSkillId = skillConfig != null ? skillConfig.SkillID : 0;
#endif
        }
        
        playingSkill = true;
@@ -857,6 +881,11 @@
#if UNITY_EDITOR
    /// <summary>卡死诊断用:暴露 playingSkillWithAnim 标志位。</summary>
    public bool PlayingSkillWithAnimForDebug => playingSkillWithAnim;
    //  [卡死诊断] 给 SkillBase 卡死报告用:谁把 playingSkillWithAnim 锁为 true、在哪一帧。
    public int PlayingSkillAnimOwnerSkillIdForDebug => _pswaSetTrueSkillId;
    public int PlayingSkillAnimOwnerFrameForDebug => _pswaSetTrueFrame;
    public string PlayingSkillAnimOwnerStackForDebug => _pswaSetTrueStack;
#endif
    public bool CanStartDeath()
Main/System/Battle/Skill/SkillBase.Cast.cs
@@ -10,6 +10,17 @@
    // 技能释放主逻辑:广播事件、高亮目标、执行释放
    public virtual void Cast()
    {
#if UNITY_EDITOR
        //  [前冲诊断] Cast() 入口:记录 skillId/caster/BattleType/当前位置/当前是否还在 tween。
        //  用来判断新技能是否在上一个技能的回位 tween 还没完成时就进入 Cast()。
        {
            Vector2 castEntryPos = (caster != null && caster.GetRectTransform() != null)
                ? caster.GetRectTransform().anchoredPosition : Vector2.zero;
            bool casterTweening = caster != null && caster.GetRectTransform() != null
                && DG.Tweening.DOTween.IsTweening(caster.GetRectTransform());
            BattleDebug.LogError($"[前冲诊断] Cast 入口 skillId={skillConfig?.SkillID} caster={caster?.ObjID} battleType={tagUseSkillAttack?.BattleType} castMode={skillSkinConfig?.castMode} anchoredPos={castEntryPos} casterTweening={casterTweening}");
        }
#endif
        // 广播技能释放事件
        string guid = battleField.guid;
        // 获取释放者数据:Hero 传递 teamHero,Mingge 传递 null(因为事件监听器只处理 Hero 数据)
@@ -163,9 +174,15 @@
    // 执行移动-施法-返回序列:通用的移动攻击流程
    private void ExecuteMoveAndCastSequence(RectTransform target, Action onReturnComplete)
    {
#if UNITY_EDITOR
        BattleDebug.LogError($"[前冲诊断] ExecuteMoveAndCastSequence 开始 skillId={skillConfig?.SkillID} caster={caster?.ObjID} battleType={tagUseSkillAttack?.BattleType} CastDistance={skillSkinConfig?.CastDistance} castMode={skillSkinConfig?.castMode}");
#endif
        ShadowIllutionCreate(true);
        MoveToTarget(target, new Vector2(skillSkinConfig.CastDistance, 0), () =>
        {
#if UNITY_EDITOR
            BattleDebug.LogError($"[前冲诊断] 前冲完成 skillId={skillConfig?.SkillID} caster={caster?.ObjID} 准备 CastImpl");
#endif
            if (skillSkinConfig.CastDistance < 9999 && skillSkinConfig.SkinllSFX2 != 0)
            {
                battleField.soundManager.PlayEffectSound(skillSkinConfig.SkinllSFX2, false);
@@ -197,18 +214,52 @@
    // 移动到目标位置:处理角色的移动动画和逻辑
    protected void MoveToTarget(RectTransform target, Vector2 offset, Action _onComplete = null, float speed = 750f)
    {
#if UNITY_EDITOR
        //  [前冲诊断] 记录入口参数:CastDistance、offset、speed;以及当前 caster 的锚点位置。
        Vector2 fromPos = caster != null && caster.GetRectTransform() != null
            ? caster.GetRectTransform().anchoredPosition : Vector2.zero;
        bool mttTweening = caster != null && caster.GetRectTransform() != null
            && DG.Tweening.DOTween.IsTweening(caster.GetRectTransform());
        BattleDebug.LogError($"[前冲诊断] MoveToTarget 入口 skillId={skillConfig?.SkillID} caster={caster?.ObjID} battleType={tagUseSkillAttack?.BattleType} CastDistance={skillSkinConfig?.CastDistance} offset={offset} speed={speed} fromPos={fromPos} casterTweening={mttTweening}");
#endif
        if (skillSkinConfig.CastDistance >= 9999)
        {
#if UNITY_EDITOR
            BattleDebug.LogError($"[前冲诊断] CastDistance>=9999 直接跳过移动 skillId={skillConfig?.SkillID} caster={caster?.ObjID}");
#endif
            _onComplete?.Invoke();
            return;
        }
        caster.PlayAnimation(MotionName.run, true);
#if UNITY_EDITOR
        //  [前冲诊断] 记录 target 的名字/世界坐标/父节点 scale,便于定位镜像坐标系导致 offset 方向反转
        string targetName = target != null ? target.name : "(null)";
        Vector3 targetWorld = target != null ? target.position : Vector3.zero;
        Vector3 targetLossyScale = target != null ? (Vector3)target.lossyScale : Vector3.one;
        Vector2 targetAnchored = target != null ? target.anchoredPosition : Vector2.zero;
        BattleDebug.LogError($"[前冲诊断] target信息 skillId={skillConfig?.SkillID} caster={caster?.ObjID} casterCamp={caster?.Camp} target.name={targetName} target.anchoredPos={targetAnchored} target.worldPos={targetWorld} target.lossyScale={targetLossyScale}");
#endif
        var tweener = BattleUtility.MoveToTarget(caster.GetRectTransform(), target, offset, () =>
        {
#if UNITY_EDITOR
            Vector2 toPos = caster != null && caster.GetRectTransform() != null
                ? caster.GetRectTransform().anchoredPosition : Vector2.zero;
            BattleDebug.LogError($"[前冲诊断] MoveToTarget 完成 skillId={skillConfig?.SkillID} caster={caster?.ObjID} toPos={toPos}");
#endif
            //  tween 完成时清除 caster 上的 activeMoveTween 句柄,放开 CanCastSkillAnimation 的闸门。
            if (caster != null)
            {
                caster.activeMoveTween = null;
            }
            caster.PlayAnimation(MotionName.idle, true);
            _onComplete?.Invoke();
        }, speed);
        //  记录到 caster,让 CanCastSkillAnimation 能精确等待这一个 tween(而不是 caster 身上任意 tween)。
        if (caster != null)
        {
            caster.activeMoveTween = tweener;
        }
        battleField.battleTweenMgr.OnPlayTween(tweener);
    }
@@ -225,6 +276,16 @@
    // 攻击完成后的处理:转身、恢复状态、播放待机动画
    protected void OnAttackFinish()
    {
#if UNITY_EDITOR
        //  [前冲诊断] OnAttackFinish 入口:记录帧号和当前位置,和 Cast()/MoveToTarget 日志对齐。
        {
            Vector2 finPos = (caster != null && caster.GetRectTransform() != null)
                ? caster.GetRectTransform().anchoredPosition : Vector2.zero;
            bool finTweening = caster != null && caster.GetRectTransform() != null
                && DG.Tweening.DOTween.IsTweening(caster.GetRectTransform());
            BattleDebug.LogError($"[前冲诊断] OnAttackFinish skillId={skillConfig?.SkillID} caster={caster?.ObjID} battleType={tagUseSkillAttack?.BattleType} anchoredPos={finPos} casterTweening={finTweening}");
        }
#endif
        TurnBack(null, 1f);
        OnAllAttackMoveFinished();
        caster.PlayAnimation(MotionName.idle, true);
Main/System/Battle/Skill/SkillBase.Finish.cs
@@ -190,6 +190,22 @@
        if (caster is HeroBattleObject hbo && hbo.motionBase != null)
        {
            casterAnim = $"  caster.motionBase: playingSkillWithAnim={hbo.motionBase.PlayingSkillWithAnimForDebug}";
            //  [卡死诊断] 当 playingSkillWithAnim=true 但本 skillBase 还没 OnSkillStart,
            //  一次性把 MotionBase 的现场也 dump 出来:锁是谁加的 + 活跃轨道列表 + 加锁时的调用栈。
            //  这样 SkillBase 的 120 帧卡死报告就能直接定位到 MotionBase 侧的 owner skill。
            if (hbo.motionBase.PlayingSkillWithAnimForDebug)
            {
                int ownerSid = hbo.motionBase.PlayingSkillAnimOwnerSkillIdForDebug;
                int ownerFrame = hbo.motionBase.PlayingSkillAnimOwnerFrameForDebug;
                int elapsed = ownerFrame > 0 ? (UnityEngine.Time.frameCount - ownerFrame) : -1;
                casterAnim += $"\n  MotionBase锁持有者: skillId={ownerSid} setFrame={ownerFrame} 已持有{elapsed}帧";
                casterAnim += $"\n  MotionBase现场: {hbo.motionBase.DumpActiveTracksForDebug()}";
                string ownerStack = hbo.motionBase.PlayingSkillAnimOwnerStackForDebug;
                if (!string.IsNullOrEmpty(ownerStack))
                {
                    casterAnim += $"\n  MotionBase锁加锁调用栈:\n{ownerStack}";
                }
            }
        }
        string skinInfo = $"  skillSkinConfig.SkillMotionName={(skillSkinConfig == null ? "null" : (string.IsNullOrEmpty(skillSkinConfig.SkillMotionName) ? "(空)" : skillSkinConfig.SkillMotionName))}";
        string skillEffectDump = skillEffect == null ? "  skillEffect=null" : $"  skillEffect: {skillEffect.DumpState()}";