Files
zeling_v2/Docs/Architecture/23_BossSkillModule.md
2026-05-08 11:04:00 +08:00

32 KiB
Raw Permalink Blame History

23 · Boss 技能模块Boss Skill Module

命名空间 BaseGames.Boss
程序集 BaseGames.Enemy(并入敌人程序集)
依赖 BaseGames.Core.Events · BaseGames.CombatDamageInfo · HitBox· BaseGames.AIBossOrchestrator · BehaviorDesigner· Kybernetik.Animancer
Design 来源 47_BossSkillSystem


目录

  1. 模块职责与层级
  2. BossSkillType 枚举
  3. VulnerabilityWindow
  4. BossSkillSO
  5. AttackPatternSO
  6. SkillSequenceSO
  7. BossSkillExecutor
  8. WeakPointSystem
  9. BossOrchestrator 集成
  10. 设计规则一览
  11. 事件频道

1. 模块职责与层级

数据层ScriptableObject
  ┌─────────────────────────────────────────────────┐
  │  BossSkillSO                                    │
  │    ├─ 技能元信息(类型/弱点窗口/互动标签)        │
  │    └─ SkillSequenceSO[]  伤害序列                │
  │         └─ AttackPatternSO[]  单个攻击图案       │
  └─────────────────────────────────────────────────┘

运行时层MonoBehaviour / UniTask
  BossOrchestratorAI 决策Behavior Designer 树)
    └─ BossSkillExecutor执行技能序列处理 VulnerabilityWindow
         ├─ WeakPointSystem弱点 HurtBox 激活管理)
         └─ HitBox[](输出伤害)

设计原则:
  ① 伤害值只写在 AttackPatternSO不在 BossSkillSO 重复
  ② 每个技能必须有 ≥1 VulnerabilityWindow后摇 ≥0.5s 或专属弱点)
  ③ 技能顺序由 BossOrchestrator 决定BossSkillExecutor 只负责执行

2. BossSkillCategory 和 BossSkillType 枚举

namespace BaseGames.Boss
{
    /// <summary>高层技能分类(平衡框架用)。</summary>
    public enum BossSkillCategory
    {
        Melee, Ranged, Charge, AoE, Environmental, Summon,
        Buff, Debuff, Phase, Passive, Reactive
    }

    /// <summary>具体技能类型(战斗设计用)。</summary>
    public enum BossSkillType
    {
        // 基础攻击
        MeleeSlash,         // 近战斩击
        ChargeAttack,       // 冲刺撞击
        LeapSlam,           // 跳起落地震
        ProjectileVolley,   // 弹幕齐射
        LaserBeam,          // 激光扫射

        // 阶段技能
        PhaseTransition,    // 阶段切换(无攻击,触发动画/护盾等)
        SummonMinion,       // 召唤小兵

        // 特殊
        ArenaTrap,          // 改变战斗区域地形
        SpeedBurst,         // 速度爆发(数帧内免疫/高速)
        DefensiveShell,     // 防御壳(需攻击弱点)
    }

    /// <summary>互动标签:定义玩家可以对该技能执行哪些反制操作。</summary>
    [Flags]
    public enum InteractionTag
    {
        None              = 0,
        Parryable         = 1 << 0,   // 可弹反
        PerfectParryOnly  = 1 << 1,   // 仅完美弹反有效
        DodgeWindow       = 1 << 2,   // 打开逃避窗口
        Unblockable       = 1 << 3,   // 无法拦截
        CanBeReflected    = 1 << 4,   // 弹幕可被反射
        ExposesWeakPoint  = 1 << 5,   // 暴露弱点
        GrantsPlayerReso  = 1 << 6,   // 多典:命中后给予玩家资源
        ArenaHazard       = 1 << 7,   // 场地危机(环境相关)
        PhaseGate         = 1 << 8,   // 阶段门关(必须触发才进入下一阶段)
    }
}

3. VulnerabilityWindow

namespace BaseGames.Boss
{
    /// <summary>弱点出现时机类型。</summary>
    public enum VulnTriggerType
    {
        OnAttackRecovery,   // 攻击后摇
        OnParriedSuccess,   // 弹反成功
        OnCounterSkillHit,  // 计算技能命中
        OnPhaseTransition,  // 阶段切换时
        OnHazardBackfire,   // 场地尃8个
        OnSummonDefeated,   // 召唤物被击败
        Manual,             // BossSkillExecutor 手动触发
    }

    /// <summary>弱点位置类型。</summary>
    public enum WeakPointType
    {
        FullBody,       // 主体全身都是弱点
        HeadOnly,       // 仅头部
        BackOnly,       // 仅背部
        CoreExposed,    // 核心暴露(展开中心 HurtBox
        CustomPoint,    // 自定义弱点 HurtBox
    }

    [Serializable]
    public struct VulnerabilityWindow
    {
        [Tooltip("弱点触发方式")]
        public VulnTriggerType TriggerType;

        [Tooltip("触发后延迟出现(秒)")]
        [Min(0f)]
        public float          TriggerDelay;

        [Tooltip("弱点持续时长(秒,设计约定 ≥0.5s")]
        [Min(0.1f)]
        public float          Duration;

        [Tooltip("弱点位置类型")]
        public WeakPointType  WeakPointType;

        [Tooltip("弱点激活时 Boss 的受击乘数1 = 正常,>1 = 额外伤害)")]
        [Min(0.1f)]
        public float          DamageMultiplier;

        [Tooltip("命中后强制驰恶")]
        public bool           ForceStagger;

        [Tooltip("驰恶时间ForceStagger=true 时生效")]
        [Min(0f)]
        public float          StaggerDuration;

        [Tooltip("弱点开启时播放的 Feedback光效等")]
        public MMF_Player     OpenFeedback;

        [Tooltip("弱点关闭时播放的 Feedback")]
        public MMF_Player     CloseFeedback;

        [Tooltip("弱点激活时的高亮颜色(指示玩家该攻击哪里)")]
        public Color          HighlightColor;

        // 与旧字段对应(保留为展示用)
        public bool  ActivateWeakPointHurtBox => WeakPointType != WeakPointType.FullBody;
    }
}

4. BossSkillSO

namespace BaseGames.Boss
{
    [CreateAssetMenu(menuName = "Boss/BossSkill")]
    public class BossSkillSO : ScriptableObject
    {
        [Header("元信息")]
        public string         skillId;
        public string         displayName;
        [TextArea(1, 4)]
        public string         designNote;       // 设计师注释(不进游戏)

        [Header("技能分类")]
        public BossSkillCategory  category;     // 高层分类Melee/Ranged 等)
        public BossSkillType      skillType;    // 具体技能类型

        [Header("阶段可用性")]
        [Tooltip("和数据层 BossPhaseConfigSO.PhaseIndex 对应;空 = 全阶段可用")]
        public int[]          availablePhaseIndices;  // 空数组 = 全阶段可用

        [Header("核心攻击动作引用")]
        [Tooltip("构成此技能的 AttackPatternSO 序列(单个技能 = 长度 1连击/多段 = 多个)")]
        public AttackPatternSO[]  attackPatterns;  // 按执行顺序排列

        [Header("弱点窗口(至少 1 个)")]
        public VulnerabilityWindow[] vulnerabilityWindows;

        [Header("互动标签(与谜题/道具联动)")]
        public InteractionTag interactionTags;   // [Flags] 枚举,替代旧 string interactionTag

        [Header("连段(执行中的子序列)")]
        public SkillSequenceSO sequenceOnHit;   // 被玩家攻击弱点时触发的序列(可空)
        public SkillSequenceSO sequenceOnMiss;  // 弱点未被攻击时触发(可空)

        [Header("玩家反制接口")]
        [Tooltip("被不同玩家行为反制时 Boss 的应激反应(见 §4.1")]
        public PlayerCounterResponse[] counterResponses;

        [Header("场景联动")]
        [Tooltip("此技能执行时触发的场景事件(见 §4.2")]
        public ArenaEventTrigger[] arenaEvents;

        [Header("Boss 资源")]
        [Tooltip("使用此技能消耗的 Boss 自身资源(见 §7")]
        public BossResourceCost resourceCost;
        [Tooltip("使用此技能后是否积累愤怒值")]
        public bool             buildsRage;

        [Header("霸体配置")]
        [Tooltip("此技能执行期间的霸体窗口(见 54_PoiseSystem §8None = 可被打断")]
        public PoiseWindowConfig poiseWindow;

        [Header("动画")]
        public ClipTransition skillAnimation;   // Animancer ClipTransition技能播放动画

        [Header("冷却")]
        [Min(0f)]
        public float          cooldown;         // 秒0 = 无冷却(由 BossOrchestrator 决策层管理)
    }
}

4.1 PlayerCounterResponse — 玩家反制接口

namespace BaseGames.Boss
{
    [Serializable]
    public struct PlayerCounterResponse
    {
        [Header("反制条件")]
        public CounterType counterType;        // 玩家用什么行为触发反制
        public string      requiredSkillId;    // counterType = SpecificSkill 时填写技能 ID

        [Header("反制效果(对 Boss")]
        public float       bossStaggerDuration;  // 强制硬直时长0 = 不强制)
        public float       bossDamageBonus;       // 对 Boss 的额外伤害倍率0 = 不额外)
        public bool        openVulnWindow;        // 是否触发 VulnerabilityWindow
        public bool        interruptSkill;        // 是否打断 Boss 当前技能

        [Header("反制收益(对玩家)")]
        public int         soulPowerGrant;        // 给予玩家的灵力数量
        public int         spiritPowerGrant;      // 给予玩家的魄元数量
        public MMF_Player  counterFeedback;       // 成功反制的特效/音效反馈
    }

    public enum CounterType
    {
        Parry,          // 弹反成功
        PerfectParry,   // 完美弹反
        DodgeThrough,   // 冲刺穿越攻击(无敌帧通过)
        SpecificSkill,  // 使用特定玩家技能(填 requiredSkillId
        WeakPointHit,   // 命中暴露的弱点
        HazardBackfire, // 利用场景危险伤到 Boss
        SummonKill,     // 击败召唤物
    }
}

4.2 ArenaEventTrigger — 场景联动

namespace BaseGames.Boss
{
    [Serializable]
    public struct ArenaEventTrigger
    {
        public string          targetArenaObjectId; // 要影响的场景物件 ID空 = 广播给所有)
        public ArenaEventType  eventType;
        public float           delay;               // 从技能触发到场景事件的延迟(秒)
        public ArenaEventParams parameters;
    }

    public enum ArenaEventType
    {
        DestroyPlatform,   // 破坏指定平台
        ActivateHazard,    // 激活陷阱(如喷火管)
        DeactivateHazard,  // 关闭陷阱
        SpawnHazardArea,   // 生成持续危险区域(熔岩/毒液)
        ShakeArena,        // 场景震动
        ToggleLighting,    // 切换场景光照
        SpawnPlatform,     // 生成新平台(阶段 2 变化)
        TriggerCutscene,   // 触发小型过场
    }

    [Serializable]
    public struct ArenaEventParams
    {
        public float duration;          // 效果持续时间0 = 永久)
        public float intensity;         // 强度(震动幅度、危险区域半径等)
        public bool  revertsOnPhaseEnd; // 阶段结束时是否恢复
    }

    [Serializable]
    public struct ArenaEventData
    {
        public ArenaEventType  type;
        public ArenaEventParams parameters;
        public string          sourceSkillId; // 来源技能 ID供场景物件做条件判断
    }

    /// <summary>
    /// 场景中可被 Boss 技能交互的对象实现此接口。
    /// Boss 通过 ArenaEventBus 广播,场景物件监听并响应。
    /// </summary>
    public interface IArenaInteractable
    {
        string ArenaObjectId { get; }
        void OnBossArenaEvent(ArenaEventData data);
    }
}

4.3 BossResourceCost — Boss 资源消耗

namespace BaseGames.Boss
{
    [Serializable]
    public struct BossResourceCost
    {
        public string resourceId;   // 对应 BossResourceConfigSO.resourceId如 "Rage"
        public float  cost;         // 消耗量0 = 不消耗资源)
        public float  minRequired;  // 使用此技能的最低资源要求
    }

    [CreateAssetMenu(menuName = "Boss/ResourceConfig")]
    public class BossResourceConfigSO : ScriptableObject
    {
        public string  resourceId;      // 如 "Rage" / "PhaseCharge"
        public string  displayName;
        public float   maxValue;
        public float   startValue;      // 初始值(通常 0

        [Header("自动变化")]
        public float   passiveRate;      // 每秒自动变化量(正=增长/负=衰减0=不变)
        public float   onTakeDamageGain; // 每受到 1 点伤害积累量
        public float   onSkillUseGain;   // 每使用一次技能积累量

        [Header("满值效果")]
        public bool       autoTriggerOnFull;
        public BossSkillSO fullTriggerSkill;
        public float      resetValueAfterTrigger;
    }
}

5. AttackPatternSO

namespace BaseGames.Boss
{
    /// <summary>
    /// 单个攻击图案的数据。
    /// 存放伤害/速度等实际参数BossSkillSO 不存参数。
    /// </summary>
    [CreateAssetMenu(menuName = "Boss/AttackPattern")]
    public class AttackPatternSO : ScriptableObject
    {
        [Header("输出")]
        public DamageSourceSO DamageSource;   // ⚠️ HitBox.Activate() 需要 DamageSourceSO架构 06 §4
        public float   KnockbackAngle;        // 击退角度(度)(基础击退力由 DamageSourceSO 内配置)

        [Header("弹幕(若为弹幕类型)")]
        public AssetReferenceGameObject ProjectilePrefab;
        public int                      ProjectileCount = 1;
        public float                    SpreadAngle     = 0f;    // 散射角(度)
        public float                    ProjectileSpeed = 8f;

        [Header("范围攻击(若为 AoE 类型)")]
        public float   AoERadius;
        public Vector2 AoEOffset;             // 相对于 Boss 的偏移

        [Header("时序")]
        public float   WindupDuration;        // 预备动作时间
        public float   ActiveDuration;        // HitBox 激活时长
        public float   RecoveryDuration;      // 后摇时长VulnerabilityWindow 通常在此段)
    }
}

6. SkillSequenceSO

namespace BaseGames.Boss
{
    /// <summary>
    /// 有序攻击序列(一个技能内的多段连段)。
    /// </summary>
    [CreateAssetMenu(menuName = "Boss/SkillSequence")]
    public class SkillSequenceSO : ScriptableObject
    {
        [Serializable]
        public struct SequenceStep
        {
            public AttackPatternSO pattern;
            public float           delayBeforeStep;  // 执行本步前等待的秒数
        }

        public SequenceStep[] steps;

        [Header("序列完成后的行为")]
        public bool     RepeatIfPlayerInRange;   // 若玩家仍在范围内则重复Survival 关卡用)
        public float    RepeatDelay;
        [Range(0, 10)]
        public int      MaxRepeatCount;          // 0 = 无限
    }
}

7. BossSkillExecutor

namespace BaseGames.Boss
{
    /// <summary>
    /// 挂在 Boss GameObject 上。
    /// 接收 BossOrchestrator 的指令,执行指定 BossSkillSO。
    /// 管理 VulnerabilityWindow 计时和 WeakPointSystem 激活。
    /// </summary>
    public class BossSkillExecutor : MonoBehaviour
    {
        [SerializeField] HitBox[]            _hitBoxes;          // Boss 身上的所有 HitBox
        [SerializeField] WeakPointSystem     _weakPointSystem;
        [SerializeField] AnimancerComponent  _animancer;
        [SerializeField] private string                  _bossId;              // Boss 资产唯一 ID
        [SerializeField] private BossSkillEventChannelSO _onBossSkillStarted;  // 发布:技能开始
        [SerializeField] private BossSkillEventChannelSO _onBossSkillEnded;    // 发布:技能结束
        // ⚠️ PlayerController 无 InstanceArchitecture 05 §2Boss 居小场景持有玩家 Transform 引用
        [SerializeField] private Transform               _playerTransform;     // 由 Inspector 指定玩家 Transform

        BossSkillSO     _currentSkill;
        AnimancerState  _currentState;
        bool            _isExecuting;
        CancellationTokenSource _cts;

        public bool IsExecuting => _isExecuting;

        // ── 公共 API ───────────────────────────────────────────────
        public async UniTask ExecuteSkill(BossSkillSO skill, CancellationToken ct)
        {
            if (_isExecuting) return;
            _isExecuting = true;
            _currentSkill = skill;
            _onBossSkillStarted?.Raise(new BossSkillEvent { BossId = _bossId, SkillId = skill.skillId });

            // 播放技能动画Animancer ClipTransition直接从 BossSkillSO 引用)
            _currentState = _animancer.Play(skill.skillAnimation);

            // 执行 SkillSequenceSO若挂载 sequenceOnMiss
            if (skill.sequenceOnMiss != null)
                await ExecuteSequence(skill.sequenceOnMiss, ct);

            _isExecuting = false;
            _onBossSkillEnded?.Raise(new BossSkillEvent { BossId = _bossId, SkillId = skill.skillId });
        }

        async UniTask ExecuteSequence(SkillSequenceSO seq, CancellationToken ct)
        {
            int repeatCount = 0;
            do
            {
                foreach (var step in seq.steps)
                {
                    ct.ThrowIfCancellationRequested();
                    await UniTask.WaitForSeconds(step.delayBeforeStep, cancellationToken: ct);
                    await ExecutePattern(step.pattern, ct);
                }
                repeatCount++;
                if (seq.RepeatDelay > 0)
                    await UniTask.WaitForSeconds(seq.RepeatDelay, cancellationToken: ct);
            }
            while (seq.RepeatIfPlayerInRange
                   && repeatCount < seq.MaxRepeatCount
                   && IsPlayerInRange()
                   && !ct.IsCancellationRequested);
        }

        async UniTask ExecutePattern(AttackPatternSO pattern, CancellationToken ct)
        {
            // 预备
            await UniTask.WaitForSeconds(pattern.WindupDuration, cancellationToken: ct);

            // 激活 HitBox 以架构 06_CombatModule §4 为准Activate(DamageSourceSO, Transform)
            foreach (var hb in _hitBoxes)
                hb.Activate(pattern.DamageSource, transform);

            await UniTask.WaitForSeconds(pattern.ActiveDuration, cancellationToken: ct);

            // 关闭 HitBox
            foreach (var hb in _hitBoxes) hb.Deactivate();

            // 后摇VulnerabilityWindow 由动画事件配合 VulnerabilityWindow 数据触发)
            await UniTask.WaitForSeconds(pattern.RecoveryDuration, cancellationToken: ct);
        }

        // ── VulnerabilityWindow 激活(由 ExecuteSkill 协程驱动)─────
        // 注意:新版 VulnerabilityWindow 改用绝对时间TriggerDelay + Duration
        // 而非归一化时间,故弃用 Update 轮询,改为 UniTask 序列激活。
        async UniTask ActivateVulnerabilityWindows(BossSkillSO skill, CancellationToken ct)
        {
            foreach (var window in skill.vulnerabilityWindows)
            {
                // 等待触发延迟
                await UniTask.Delay(
                    TimeSpan.FromSeconds(window.TriggerDelay),
                    cancellationToken: ct);

                // 激活弱点
                bool isCustom = window.ActivateWeakPointHurtBox;
                _weakPointSystem.SetActive(true, window.DamageMultiplier, isCustom);
                window.OpenFeedback?.PlayFeedbacks();

                // 持续弱点窗口
                await UniTask.Delay(
                    TimeSpan.FromSeconds(window.Duration),
                    cancellationToken: ct);

                // 关闭弱点
                _weakPointSystem.SetActive(false, 1f, isCustom);
                window.CloseFeedback?.PlayFeedbacks();
            }
        }

        bool IsPlayerInRange() =>
            _playerTransform != null &&
            Vector2.Distance(transform.position, _playerTransform.position) < 8f;
    }
}

8. WeakPointSystem

namespace BaseGames.Boss
{
    /// <summary>
    /// 管理 Boss 的专属弱点 HurtBox如核心、眼睛等
    /// 弱点激活期间受到的伤害会乘以 DamageMultiplier。
    /// </summary>
    public class WeakPointSystem : MonoBehaviour
    {
        [Serializable]
        public struct WeakPoint
        {
            public HurtBox      hurtBox;
            public GameObject   visualIndicator;   // 弱点亮光/标记(可为 null
        }

        [SerializeField] WeakPoint[] _weakPoints;
        [SerializeField] private string                _bossId;                       // Boss 资产唯一 ID
        [SerializeField] private StringEventChannelSO  _onVulnerabilityWindowOpened;  // 发布:弱点窗口开启

        float _damageMultiplier = 1f;

        public void SetActive(bool active, float multiplier = 1f)
        {
            _damageMultiplier = multiplier;
            foreach (var wp in _weakPoints)
            {
                wp.hurtBox.gameObject.SetActive(active);
                if (wp.visualIndicator != null)
                    wp.visualIndicator.SetActive(active);
            }
            if (active) _onVulnerabilityWindowOpened?.Raise(_bossId);
        }

        /// <summary>
        /// 弱点 HurtBox 受击时,由 BossStats 调用此方法获取最终伤害系数。
        /// </summary>
        public float GetDamageMultiplier() => _damageMultiplier;
    }
}

9. BossOrchestrator 集成

BossOrchestrator 是 Behavior Designer 的宿主 MonoBehaviour持有 BossSkillExecutor 引用:

// BossOrchestrator 片段(供 BD 节点调用)
public class BossOrchestrator : MonoBehaviour
{
    [SerializeField] BossSkillSO[]      _phaseOneSkills;
    [SerializeField] BossSkillSO[]      _phaseTwoSkills;
    [SerializeField] BossSkillExecutor  _executor;

    int   _currentPhase = 1;
    CancellationTokenSource _cts;

    // BD Task 节点调用ExecuteSkill(skillId)
    public async UniTask ExecuteSkillById(string skillId)
    {
        var skills = _currentPhase == 1 ? _phaseOneSkills : _phaseTwoSkills;
        var skill  = System.Array.Find(skills, s => s.skillId == skillId);
        if (skill == null) return;

        _cts?.Cancel();
        _cts = new CancellationTokenSource();
        await _executor.ExecuteSkill(skill, _cts.Token);
    }

    public void EnterPhaseTwo()
    {
        _currentPhase = 2;
        _cts?.Cancel(); // 打断当前技能
    }
}

10. 设计规则一览

规则 说明
每个技能 ≥1 VulnerabilityWindow 后摇窗口 ≥0.5s(归一化时长约 0.15~0.4
伤害参数只在 AttackPatternSO BossSkillSO 不存 BaseDamage 等参数
技能冷却由 BossOrchestrator 管理 BossSkillExecutor 只执行,不维护冷却
VulnerabilityWindow 在编辑器中校验 自定义 Validator Tool 检查 DurationNormalized ≥ 0.1
Boss 阶段切换打断当前技能 _cts.Cancel() 终止 UniTask 序列

11. 事件频道

频道 SO Payload 发布者 订阅者
EVT_BossSkillStarted (string bossId, string skillId) BossSkillExecutor BossHUD(显示技能名)(震动通过 CameraStateController.Instance.TriggerImpulse() 直接调用,不订阅事件)
EVT_BossSkillEnded (string bossId, string skillId) BossSkillExecutor BossOrchestratorBD 决策推进)
EVT_BossVulnerabilityWindowOpened string bossId WeakPointSystem PlayerFeedback(提示音效)、HUDController
EVT_BossPhaseChanged (string bossId, int phase) BossBase.EnterPhase() BossHUD(相机切换通过 CameraStateController.Instance 直接调用,不订阅事件)

12. BossSkillSequenceEditorWindow — 技能序列可视化

痛点SkillSequenceSO 包含多个 SequenceStep[]每步AttackPatternSO + delayBeforeStep但 Inspector 中只能逐字段查看数字,策划无法直觉感受技能序列的节奏感——哪段是攻击前摇、哪段是弱点窗口、总时长是否合理,全部要靠心算。本 EditorWindow 以 甘特图Gantt Chart 方式将时间线可视化。

12.1 功能规格

功能 说明
时间轴刻度 横轴为时间(秒),最大显示 SkillSequenceSO.TotalDuration,刻度精度 0.1s
攻击阶段条 每个 AttackPatternSO 渲染为蓝色横条,长度 = pattern.Duration
延迟段 delayBeforeStep 渲染为灰色空隙
弱点窗口 VulnerabilityWindowstartNormalized~endNormalized)渲染为绿色覆盖条
选中高亮 点击任一条可选中对应 AttackPatternSOInspector 同步 Ping
验证警告 VulnerabilityWindow.DurationNormalized < 0.1 时对应条变红 + Tooltip 提示
SO 拖放 SkillSequenceSO 资产拖入窗口即可加载

12.2 实现规范

// 路径: Assets/Scripts/Editor/BossSkill/BossSkillSequenceWindow.cs
// 程序集: BaseGames.EditorEditor Only
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;

namespace BaseGames.Editor.BossSkill
{
    public class BossSkillSequenceWindow : EditorWindow
    {
        [MenuItem("BaseGames/Tools/Boss Skill Sequence Viewer")]
        public static void Open() => GetWindow<BossSkillSequenceWindow>("技能序列查看器");

        // ── 状态 ──────────────────────────────────────────────────────────────
        private SkillSequenceSO _target;
        private Vector2         _scroll;
        private float           _zoom         = 100f;    // 像素/秒
        private const float     TrackHeight   = 28f;
        private const float     LabelWidth    = 140f;
        private const float     HeaderHeight  = 30f;

        // 颜色
        private static readonly Color ColDelay       = new(0.3f, 0.3f, 0.3f, 0.5f);
        private static readonly Color ColAttack      = new(0.2f, 0.5f, 1.0f, 0.8f);
        private static readonly Color ColVulnOk      = new(0.2f, 0.8f, 0.3f, 0.6f);
        private static readonly Color ColVulnWarn    = new(1.0f, 0.3f, 0.3f, 0.7f);

        private void OnGUI()
        {
            DrawToolbar();
            if (_target == null)
            {
                EditorGUILayout.HelpBox("将 SkillSequenceSO 拖到此处或从 Toolbar 选择", MessageType.Info);
                HandleDragDrop();
                return;
            }
            DrawTimeline();
        }

        private void DrawToolbar()
        {
            EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
            _target = (SkillSequenceSO)EditorGUILayout.ObjectField(
                "技能序列", _target, typeof(SkillSequenceSO), false,
                GUILayout.Width(300));
            GUILayout.Label($"缩放: {_zoom:0}px/s");
            _zoom = GUILayout.HorizontalSlider(_zoom, 30f, 300f, GUILayout.Width(100));
            EditorGUILayout.EndHorizontal();
        }

        private void DrawTimeline()
        {
            float totalSeconds = _target.TotalDuration;
            float timelineWidth = totalSeconds * _zoom + LabelWidth + 20f;

            _scroll = EditorGUILayout.BeginScrollView(_scroll);
            Rect viewRect = EditorGUILayout.GetControlRect(false,
                HeaderHeight + _target.Steps.Length * (TrackHeight + 4f),
                GUILayout.Width(timelineWidth));

            DrawRuler(viewRect, totalSeconds);

            float trackY = viewRect.y + HeaderHeight;
            float cursor = 0f;  // 当前时间游标(秒)

            for (int i = 0; i < _target.Steps.Length; i++)
            {
                var step = _target.Steps[i];
                Rect trackRect = new(viewRect.x, trackY + i * (TrackHeight + 4f),
                    viewRect.width, TrackHeight);

                // 标签
                Rect labelRect = new(trackRect.x, trackRect.y, LabelWidth, TrackHeight);
                GUI.Label(labelRect, step.Pattern?.name ?? "—", EditorStyles.miniLabel);

                // 延迟段
                if (step.DelayBeforeStep > 0f)
                {
                    Rect delayRect = TimeToRect(trackRect, cursor, step.DelayBeforeStep);
                    EditorGUI.DrawRect(delayRect, ColDelay);
                    cursor += step.DelayBeforeStep;
                }

                // 攻击段
                float patternDur = step.Pattern?.Duration ?? 0f;
                if (patternDur > 0f)
                {
                    Rect atkRect = TimeToRect(trackRect, cursor, patternDur);
                    EditorGUI.DrawRect(atkRect, ColAttack);
                    GUI.Label(atkRect, step.Pattern?.name ?? "", EditorStyles.centeredGreyMiniLabel);

                    // 弱点窗口覆盖层
                    DrawVulnerabilityWindows(atkRect, cursor, patternDur, step.Pattern);
                    cursor += patternDur;
                }
            }

            EditorGUILayout.EndScrollView();
        }

        private void DrawVulnerabilityWindows(Rect atkRect, float patternStart,
            float patternDur, AttackPatternSO pattern)
        {
            if (pattern?.VulnerabilityWindows == null) return;
            foreach (var vw in pattern.VulnerabilityWindows)
            {
                float wStart = vw.StartNormalized * patternDur;
                float wDur   = (vw.EndNormalized - vw.StartNormalized) * patternDur;
                bool  warn   = vw.DurationNormalized < 0.1f;
                Color col    = warn ? ColVulnWarn : ColVulnOk;

                Rect wRect = TimeToRect(atkRect, patternStart + wStart, wDur);
                EditorGUI.DrawRect(wRect, col);
                if (warn)
                    GUI.Label(wRect, "⚠", new GUIStyle { fontSize = 10,
                        normal = { textColor = Color.white }, alignment = TextAnchor.MiddleCenter });
            }
        }

        private Rect TimeToRect(Rect trackRect, float startSec, float durSec)
        {
            float x = trackRect.x + LabelWidth + startSec * _zoom;
            float w = Mathf.Max(2f, durSec * _zoom);
            return new Rect(x, trackRect.y + 2f, w, trackRect.height - 4f);
        }

        private void DrawRuler(Rect viewRect, float totalSeconds)
        {
            float step = _zoom >= 80f ? 0.5f : 1f;
            for (float t = 0; t <= totalSeconds; t += step)
            {
                float x = viewRect.x + LabelWidth + t * _zoom;
                EditorGUI.DrawRect(new Rect(x, viewRect.y, 1f, HeaderHeight), Color.gray);
                GUI.Label(new Rect(x + 2f, viewRect.y, 40f, 16f), $"{t:0.0}s",
                    EditorStyles.miniLabel);
            }
        }

        private void HandleDragDrop()
        {
            var evt = Event.current;
            if (evt.type == EventType.DragUpdated || evt.type == EventType.DragPerform)
            {
                DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
                if (evt.type == EventType.DragPerform)
                {
                    DragAndDrop.AcceptDrag();
                    foreach (var obj in DragAndDrop.objectReferences)
                        if (obj is SkillSequenceSO seq) { _target = seq; break; }
                }
                evt.Use();
            }
        }
    }
}
#endif

打开方式:菜单 BaseGames → Tools → Boss Skill Sequence Viewer,将 SkillSequenceSO 资产拖入即可。时间轴缩放通过 Toolbar 滑条控制;弱点窗口 DurationNormalized < 0.1 时变红警告对应设计规则§10 Validator 等价)。