refactor(enemy)!: 删除旧 Boss 轨 9 个脚本
BossSkillSO / AttackPatternSO / SkillSequenceSO / BossSkillExecutor / BossSkillTypes / WeakPointSystem / TelegraphSystem / BossSkillModule / BossSkillSequenceWindow,连同 Patterns 独立程序集(BaseGames.Enemies.Boss.Patterns.asmdef, 已核实无任何 asmdef 按名或按 GUID 引用它)整包移除。 删前先清两处引用: - ChaoFengKnockdownCounter 击落打断改走 _boss.Abilities.InterruptAll(ExternalRequest) - EditorScaffoldUtils 命名前缀表删掉两条指向已删类型的死条目(还与 AssetFolderSpec 的 ABL_ 冲突) 判据「有更优替代才删」逐项对应: - 编排 → EnemyAbilitySO + EnemyAttackSO(归一化时机 / 槽位 HitBox) - 阶段门 → BossPhaseAbilityGate - 选招 → EnemyAttackSelector + WeightedRandomAntiRepeat - 可弹反 → DamageSourceSO.Flags(CanBeParried),HurtBox 已在判定 - 弱点倍率 → HurtBox 自带倍率(原实现的 GetDamageMultiplier 零调用者) - 预警 → clip 姿态 + 动画事件(TriggerFeedback / PlaySFX 已补接) - 竞技场联动 → EnemySpawnerOnEvent - 霸体窗口 → EnemyAttackSO.hasPoiseWindow 四字段 能力资产的编辑器总览视图随 BossSkillModule 一并消失, 后续补 EnemyAbilityModule(优先级低于 AiDefinitionValidator)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,306 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.Boss;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Boss 技能序列甘特图可视化窗口(架构 23_BossSkillModule §12)。
|
||||
/// 菜单:BaseGames/Tools/Boss Skill Sequence Viewer
|
||||
///
|
||||
/// 功能:
|
||||
/// - 拖放 BossSkillSO 或 SkillSequenceSO 资产加载
|
||||
/// - 甘特图:Windup(黄色)→ Active(红色)→ Recovery(灰色)各阶段时序条
|
||||
/// - VulnerabilityWindow 绿色覆盖层(TriggerDelay 偏移 + Duration 宽度)
|
||||
/// - DurationNormalized < 0.1 时阶段条变红警告
|
||||
/// - 点击阶段条高亮对应 AttackPatternSO(EditorGUIUtility.PingObject)
|
||||
/// </summary>
|
||||
public class BossSkillSequenceWindow : EditorWindow
|
||||
{
|
||||
// ── State ──────────────────────────────────────────────────────────
|
||||
private BossSkillSO _loadedSkill;
|
||||
private SkillSequenceSO _loadedSequence;
|
||||
private Vector2 _scrollPos;
|
||||
|
||||
// ── Layout ─────────────────────────────────────────────────────────
|
||||
private const float HeaderH = 24f;
|
||||
private const float RowH = 28f;
|
||||
private const float LabelW = 180f;
|
||||
private const float MinBarWidth = 6f;
|
||||
// 时间轴宽度随窗口宽度动态调整,最小 300px
|
||||
private float TimelineW => Mathf.Max(300f, position.width - LabelW - 30f);
|
||||
|
||||
// ── Colors ─────────────────────────────────────────────────────────
|
||||
private static readonly Color ColWindup = new Color(0.95f, 0.80f, 0.10f, 0.85f);
|
||||
private static readonly Color ColActive = new Color(0.90f, 0.20f, 0.15f, 0.85f);
|
||||
private static readonly Color ColRecovery = new Color(0.50f, 0.50f, 0.55f, 0.70f);
|
||||
private static readonly Color ColVuln = new Color(0.10f, 0.90f, 0.30f, 0.45f);
|
||||
private static readonly Color ColDelay = new Color(0.25f, 0.25f, 0.30f, 0.50f);
|
||||
private static readonly Color ColWarn = new Color(0.95f, 0.10f, 0.10f, 0.85f);
|
||||
|
||||
[MenuItem("BaseGames/Data/Boss Skill Sequence", priority = 110)]
|
||||
public static void OpenWindow()
|
||||
{
|
||||
var win = GetWindow<BossSkillSequenceWindow>("Boss Skill Sequence");
|
||||
win.minSize = new Vector2(900, 400);
|
||||
win.Show();
|
||||
}
|
||||
|
||||
// ── GUI ────────────────────────────────────────────────────────────
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
DrawToolbar();
|
||||
|
||||
if (_loadedSkill == null && _loadedSequence == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"将 BossSkillSO 或 SkillSequenceSO 资产拖放到此处,或使用上方字段加载。",
|
||||
MessageType.Info);
|
||||
HandleDragDrop();
|
||||
return;
|
||||
}
|
||||
|
||||
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
|
||||
|
||||
if (_loadedSkill != null)
|
||||
DrawSkillTimeline(_loadedSkill);
|
||||
else if (_loadedSequence != null)
|
||||
DrawSequenceTimeline(_loadedSequence);
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
// ── Toolbar ───────────────────────────────────────────────────────
|
||||
|
||||
private void DrawToolbar()
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||||
|
||||
EditorGUILayout.LabelField("技能:", GUILayout.Width(36));
|
||||
var newSkill = (BossSkillSO)EditorGUILayout.ObjectField(
|
||||
_loadedSkill, typeof(BossSkillSO), false, GUILayout.Width(200));
|
||||
if (newSkill != _loadedSkill)
|
||||
{
|
||||
_loadedSkill = newSkill;
|
||||
_loadedSequence = null;
|
||||
}
|
||||
|
||||
GUILayout.Space(12);
|
||||
EditorGUILayout.LabelField("序列:", GUILayout.Width(36));
|
||||
var newSeq = (SkillSequenceSO)EditorGUILayout.ObjectField(
|
||||
_loadedSequence, typeof(SkillSequenceSO), false, GUILayout.Width(200));
|
||||
if (newSeq != _loadedSequence)
|
||||
{
|
||||
_loadedSequence = newSeq;
|
||||
_loadedSkill = null;
|
||||
}
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
if (GUILayout.Button("清除", EditorStyles.toolbarButton, GUILayout.Width(50)))
|
||||
{
|
||||
_loadedSkill = null;
|
||||
_loadedSequence = null;
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
// ── BossSkillSO 时间轴 ────────────────────────────────────────────
|
||||
|
||||
private void DrawSkillTimeline(BossSkillSO skill)
|
||||
{
|
||||
EditorGUILayout.LabelField($"技能:{skill.displayName} [{skill.skillId}]",
|
||||
EditorStyles.boldLabel);
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
if (skill.attackPatterns == null || skill.attackPatterns.Length == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox("此技能没有 AttackPattern。", MessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算总时长
|
||||
float totalDuration = 0f;
|
||||
foreach (var p in skill.attackPatterns)
|
||||
if (p != null) totalDuration += p.WindupDuration + p.ActiveDuration + p.RecoveryDuration;
|
||||
|
||||
if (totalDuration <= 0f) totalDuration = 1f;
|
||||
|
||||
DrawTimelineHeader(totalDuration);
|
||||
|
||||
float cursor = 0f;
|
||||
for (int i = 0; i < skill.attackPatterns.Length; i++)
|
||||
{
|
||||
var pattern = skill.attackPatterns[i];
|
||||
if (pattern == null) continue;
|
||||
DrawPatternRow($"[{i}] {pattern.name}", pattern, ref cursor, totalDuration);
|
||||
}
|
||||
|
||||
// 绘制 VulnerabilityWindows
|
||||
if (skill.vulnerabilityWindows != null && skill.vulnerabilityWindows.Length > 0)
|
||||
{
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.LabelField("弱点窗口(Vulnerability Windows)", EditorStyles.miniBoldLabel);
|
||||
foreach (var vw in skill.vulnerabilityWindows)
|
||||
DrawVulnWindowRow(vw, totalDuration);
|
||||
}
|
||||
}
|
||||
|
||||
// ── SkillSequenceSO 时间轴 ────────────────────────────────────────
|
||||
|
||||
private void DrawSequenceTimeline(SkillSequenceSO sequence)
|
||||
{
|
||||
EditorGUILayout.LabelField($"序列:{sequence.name}", EditorStyles.boldLabel);
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
if (sequence.steps == null || sequence.steps.Length == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox("此序列没有步骤。", MessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算总时长
|
||||
float totalDuration = 0f;
|
||||
foreach (var step in sequence.steps)
|
||||
{
|
||||
totalDuration += step.delayBeforeStep;
|
||||
if (step.pattern != null)
|
||||
totalDuration += step.pattern.WindupDuration + step.pattern.ActiveDuration + step.pattern.RecoveryDuration;
|
||||
}
|
||||
if (totalDuration <= 0f) totalDuration = 1f;
|
||||
|
||||
DrawTimelineHeader(totalDuration);
|
||||
|
||||
float cursor = 0f;
|
||||
for (int i = 0; i < sequence.steps.Length; i++)
|
||||
{
|
||||
var step = sequence.steps[i];
|
||||
|
||||
// 延迟条
|
||||
if (step.delayBeforeStep > 0f)
|
||||
{
|
||||
DrawBar($"延迟 {step.delayBeforeStep:F2}s", cursor, step.delayBeforeStep,
|
||||
totalDuration, ColDelay, null);
|
||||
cursor += step.delayBeforeStep;
|
||||
}
|
||||
|
||||
if (step.pattern != null)
|
||||
DrawPatternRow($"[{i}] {step.pattern.name}", step.pattern, ref cursor, totalDuration);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 共用绘制方法 ──────────────────────────────────────────────────
|
||||
|
||||
private void DrawTimelineHeader(float totalDuration)
|
||||
{
|
||||
Rect headerRect = EditorGUILayout.GetControlRect(false, HeaderH);
|
||||
headerRect.x += LabelW;
|
||||
headerRect.width -= LabelW;
|
||||
|
||||
EditorGUI.DrawRect(headerRect, new Color(0.18f, 0.18f, 0.20f));
|
||||
|
||||
// 刻度线(每 0.5s 一条)
|
||||
float step = 0.5f;
|
||||
for (float t = 0; t <= totalDuration + 0.001f; t += step)
|
||||
{
|
||||
float x = headerRect.x + (t / totalDuration) * headerRect.width;
|
||||
EditorGUI.DrawRect(new Rect(x, headerRect.y, 1f, HeaderH * 0.6f), Color.gray);
|
||||
EditorGUI.LabelField(new Rect(x + 2f, headerRect.y, 40f, HeaderH),
|
||||
$"{t:F1}s", new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = Color.gray } });
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPatternRow(string label, AttackPatternSO pattern, ref float cursor, float totalDuration)
|
||||
{
|
||||
float windupDur = pattern.WindupDuration;
|
||||
float activeDur = pattern.ActiveDuration;
|
||||
float recoveryDur = pattern.RecoveryDuration;
|
||||
|
||||
float rowStart = cursor;
|
||||
EditorGUILayout.BeginHorizontal(GUILayout.Height(RowH));
|
||||
|
||||
// 标签 + Ping
|
||||
if (GUILayout.Button(label, EditorStyles.miniLabel, GUILayout.Width(LabelW), GUILayout.Height(RowH)))
|
||||
EditorGUIUtility.PingObject(pattern);
|
||||
|
||||
Rect timelineRect = EditorGUILayout.GetControlRect(false, RowH,
|
||||
GUILayout.Width(TimelineW));
|
||||
|
||||
// Windup
|
||||
if (windupDur > 0f)
|
||||
DrawBarInRect(timelineRect, cursor, windupDur, totalDuration,
|
||||
windupDur / (windupDur + activeDur + recoveryDur) < 0.1f ? ColWarn : ColWindup);
|
||||
cursor += windupDur;
|
||||
|
||||
// Active
|
||||
if (activeDur > 0f)
|
||||
DrawBarInRect(timelineRect, cursor, activeDur, totalDuration,
|
||||
activeDur / (windupDur + activeDur + recoveryDur) < 0.1f ? ColWarn : ColActive);
|
||||
cursor += activeDur;
|
||||
|
||||
// Recovery
|
||||
if (recoveryDur > 0f)
|
||||
DrawBarInRect(timelineRect, cursor, recoveryDur, totalDuration,
|
||||
recoveryDur / (windupDur + activeDur + recoveryDur) < 0.1f ? ColWarn : ColRecovery);
|
||||
cursor += recoveryDur;
|
||||
|
||||
_ = rowStart; // suppress unused warning
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void DrawVulnWindowRow(VulnerabilityWindow vw, float totalDuration)
|
||||
{
|
||||
string label = $"弱点:{vw.TriggerType} +{vw.TriggerDelay:F2}s / {vw.Duration:F2}s";
|
||||
DrawBar(label, vw.TriggerDelay, vw.Duration, totalDuration, ColVuln, null);
|
||||
}
|
||||
|
||||
private void DrawBar(string label, float start, float duration, float totalDuration,
|
||||
Color color, AttackPatternSO pingTarget)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal(GUILayout.Height(RowH));
|
||||
|
||||
if (GUILayout.Button(label, EditorStyles.miniLabel, GUILayout.Width(LabelW), GUILayout.Height(RowH)))
|
||||
{
|
||||
if (pingTarget != null) EditorGUIUtility.PingObject(pingTarget);
|
||||
}
|
||||
|
||||
Rect timelineRect = EditorGUILayout.GetControlRect(false, RowH, GUILayout.Width(TimelineW));
|
||||
DrawBarInRect(timelineRect, start, duration, totalDuration, color);
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private static void DrawBarInRect(Rect timeline, float start, float duration,
|
||||
float totalDuration, Color color)
|
||||
{
|
||||
float x = timeline.x + (start / totalDuration) * timeline.width;
|
||||
float w = Mathf.Max(MinBarWidth, (duration / totalDuration) * timeline.width);
|
||||
EditorGUI.DrawRect(new Rect(x, timeline.y + 2f, w, timeline.height - 4f), color);
|
||||
}
|
||||
|
||||
// ── Drag & Drop ───────────────────────────────────────────────────
|
||||
|
||||
private void HandleDragDrop()
|
||||
{
|
||||
var evt = Event.current;
|
||||
if (evt.type != EventType.DragUpdated && evt.type != EventType.DragPerform) return;
|
||||
|
||||
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
|
||||
|
||||
if (evt.type == EventType.DragPerform)
|
||||
{
|
||||
DragAndDrop.AcceptDrag();
|
||||
foreach (var obj in DragAndDrop.objectReferences)
|
||||
{
|
||||
if (obj is BossSkillSO skill) { _loadedSkill = skill; _loadedSequence = null; break; }
|
||||
if (obj is SkillSequenceSO seq) { _loadedSequence = seq; _loadedSkill = null; break; }
|
||||
}
|
||||
Repaint();
|
||||
}
|
||||
evt.Use();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d47145d394333184eb3ff822e3c4aa4d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,209 +0,0 @@
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using BaseGames.Boss;
|
||||
|
||||
namespace BaseGames.Editor.Modules
|
||||
{
|
||||
/// <summary>
|
||||
/// DataHub Boss技能模块 —— Tab 切换管理 BossSkillSO 和 SkillSequenceSO。
|
||||
/// </summary>
|
||||
public class BossSkillModule : IDataModule, IDataModuleOrdered
|
||||
{
|
||||
private const string SkillFolder = "Assets/_Game/Data/Boss/Skills";
|
||||
private const string SeqFolder = "Assets/_Game/Data/Boss/Sequences";
|
||||
|
||||
public string ModuleId => "boss";
|
||||
public string DisplayName => "Boss技能";
|
||||
public string IconName => null;
|
||||
public int DisplayOrder => 50;
|
||||
|
||||
private int _activeTab = 0;
|
||||
|
||||
private SoListPane<BossSkillSO> _skillPane;
|
||||
private SoListPane<SkillSequenceSO> _seqPane;
|
||||
private Action<UnityEngine.Object> _onSelected;
|
||||
|
||||
private DetailHeader _header;
|
||||
private BossSkillSO _selectedSkill;
|
||||
private SkillSequenceSO _selectedSeq;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_skillPane = new SoListPane<BossSkillSO>(
|
||||
SkillFolder, "ABL_Boss_",
|
||||
s => s.category.ToString());
|
||||
_skillPane.SelectionChanged = s => { _selectedSkill = s; _onSelected?.Invoke(s); };
|
||||
|
||||
_seqPane = new SoListPane<SkillSequenceSO>(SeqFolder, "ABL_Seq_");
|
||||
_seqPane.SelectionChanged = s => { _selectedSeq = s; _onSelected?.Invoke(s); };
|
||||
}
|
||||
|
||||
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
|
||||
{
|
||||
_onSelected = onSelected;
|
||||
container.style.flexDirection = FlexDirection.Column;
|
||||
|
||||
// Tab bar
|
||||
var tabBar = new VisualElement();
|
||||
tabBar.style.flexDirection = FlexDirection.Row;
|
||||
tabBar.style.borderBottomWidth = 1;
|
||||
tabBar.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.3f));
|
||||
container.Add(tabBar);
|
||||
|
||||
var btnSkill = BuildTabBtn("技能 (Skill)", 0, tabBar);
|
||||
var btnSeq = BuildTabBtn("序列 (Seq)", 1, tabBar);
|
||||
|
||||
var listArea = new VisualElement();
|
||||
listArea.style.flexGrow = 1;
|
||||
container.Add(listArea);
|
||||
|
||||
ShowTab(0, listArea, new[] { btnSkill, btnSeq });
|
||||
btnSkill.clicked += () => ShowTab(0, listArea, new[] { btnSkill, btnSeq });
|
||||
btnSeq.clicked += () => ShowTab(1, listArea, new[] { btnSkill, btnSeq });
|
||||
|
||||
_skillPane.Refresh();
|
||||
_seqPane.Refresh();
|
||||
}
|
||||
|
||||
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
|
||||
{
|
||||
_header = new DetailHeader();
|
||||
_header.SetAsset(selected);
|
||||
_header.RenameRequested += name => OnRenameRequested(selected, name);
|
||||
container.Add(_header);
|
||||
|
||||
if (selected == null) return;
|
||||
|
||||
if (selected is BossSkillSO skill)
|
||||
{
|
||||
container.Add(BuildSkillCard(skill));
|
||||
container.Add(BuildActionBar(skill, SkillFolder, _skillPane));
|
||||
container.Add(SkillModule.MakeDivider());
|
||||
var insp = new InspectorElement(skill); container.Add(insp);
|
||||
}
|
||||
else if (selected is SkillSequenceSO seq)
|
||||
{
|
||||
container.Add(BuildSeqCard(seq));
|
||||
container.Add(BuildActionBar(seq, SeqFolder, _seqPane));
|
||||
container.Add(SkillModule.MakeDivider());
|
||||
var insp = new InspectorElement(seq); container.Add(insp);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnActivated()
|
||||
{
|
||||
_skillPane?.Refresh();
|
||||
_seqPane?.Refresh();
|
||||
}
|
||||
|
||||
// ── 内部 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private Button BuildTabBtn(string text, int tabIdx, VisualElement bar)
|
||||
{
|
||||
var btn = new Button { text = text };
|
||||
btn.style.flexGrow = 1;
|
||||
btn.style.paddingTop = 5;
|
||||
btn.style.paddingBottom = 5;
|
||||
btn.style.borderTopLeftRadius = 0;
|
||||
btn.style.borderTopRightRadius = 0;
|
||||
btn.style.borderBottomLeftRadius = 0;
|
||||
btn.style.borderBottomRightRadius = 0;
|
||||
btn.style.borderLeftWidth = 0;
|
||||
btn.style.borderRightWidth = 0;
|
||||
btn.style.borderTopWidth = 0;
|
||||
btn.style.borderBottomWidth = 0;
|
||||
btn.style.backgroundColor = new StyleColor(Color.clear);
|
||||
btn.userData = tabIdx;
|
||||
bar.Add(btn);
|
||||
return btn;
|
||||
}
|
||||
|
||||
private void ShowTab(int tab, VisualElement area, Button[] tabBtns)
|
||||
{
|
||||
_activeTab = tab;
|
||||
area.Clear();
|
||||
|
||||
for (int i = 0; i < tabBtns.Length; i++)
|
||||
{
|
||||
if (i == tab)
|
||||
{
|
||||
tabBtns[i].style.borderBottomWidth = 2;
|
||||
tabBtns[i].style.borderBottomColor = new StyleColor(new Color(0.4f, 0.65f, 1f, 1f));
|
||||
tabBtns[i].style.opacity = 1f;
|
||||
}
|
||||
else
|
||||
{
|
||||
tabBtns[i].style.borderBottomWidth = 0;
|
||||
tabBtns[i].style.opacity = 0.65f;
|
||||
}
|
||||
}
|
||||
|
||||
if (tab == 0) { _skillPane.style.flexGrow = 1; area.Add(_skillPane); }
|
||||
else { _seqPane.style.flexGrow = 1; area.Add(_seqPane); }
|
||||
}
|
||||
|
||||
private void OnRenameRequested(UnityEngine.Object asset, string newName)
|
||||
{
|
||||
var (ok, err) = AssetOperations.Rename(asset, newName);
|
||||
if (!ok) EditorUtility.DisplayDialog("重命名失败", err, "确定");
|
||||
else
|
||||
{
|
||||
_header.SetAsset(asset);
|
||||
if (_activeTab == 0) _skillPane.Invalidate();
|
||||
else _seqPane.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private static VisualElement BuildSkillCard(BossSkillSO s)
|
||||
{
|
||||
var card = SkillModule.MakeCard();
|
||||
SkillModule.AddChip(card, "分类", s.category.ToString());
|
||||
SkillModule.AddChip(card, "类型", s.skillType.ToString());
|
||||
SkillModule.AddChip(card, "模式数", (s.attackPatterns?.Length ?? 0).ToString());
|
||||
SkillModule.AddChip(card, "弱点窗口", (s.vulnerabilityWindows?.Length ?? 0).ToString());
|
||||
if (!string.IsNullOrEmpty(s.skillId))
|
||||
SkillModule.AddChip(card, "ID", s.skillId);
|
||||
return card;
|
||||
}
|
||||
|
||||
private static VisualElement BuildSeqCard(SkillSequenceSO s)
|
||||
{
|
||||
var card = SkillModule.MakeCard();
|
||||
SkillModule.AddChip(card, "步骤数", (s.steps?.Length ?? 0).ToString());
|
||||
SkillModule.AddChip(card, "循环", s.RepeatIfPlayerInRange ? "是" : "否");
|
||||
SkillModule.AddChip(card, "最大循环次数", s.MaxRepeatCount.ToString());
|
||||
return card;
|
||||
}
|
||||
|
||||
private VisualElement BuildActionBar<T>(T asset, string folder, SoListPane<T> pane)
|
||||
where T : ScriptableObject
|
||||
{
|
||||
var bar = SkillModule.MakeActionBar();
|
||||
new Button(() => { EditorGUIUtility.PingObject(asset); Selection.activeObject = asset; })
|
||||
{ text = "定位" }.AlsoAddTo(bar);
|
||||
new Button(() =>
|
||||
{
|
||||
var c = AssetOperations.Clone(asset, folder);
|
||||
if (c != null) pane.Refresh(c);
|
||||
}) { text = "克隆..." }.AlsoAddTo(bar);
|
||||
var del = new Button(() =>
|
||||
{
|
||||
if (AssetOperations.Delete(asset)) pane.Refresh(null);
|
||||
}) { text = "删除" };
|
||||
del.style.borderLeftColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
|
||||
del.style.borderRightColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
|
||||
del.style.borderTopColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
|
||||
del.style.borderBottomColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
|
||||
del.style.borderLeftWidth = 1;
|
||||
del.style.borderRightWidth = 1;
|
||||
del.style.borderTopWidth = 1;
|
||||
del.style.borderBottomWidth = 1;
|
||||
del.style.marginLeft = 8;
|
||||
del.AlsoAddTo(bar);
|
||||
return bar;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0d0425e529293e469da3762fe3bf8f0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -18,8 +18,6 @@ namespace BaseGames.Editor
|
||||
{
|
||||
{ "WeaponSO", ("WPN_", "WPN_{ID},例:WPN_SkyBlade") },
|
||||
{ "FormSkillSO", ("SKL_", "SKL_{Name},例:SKL_SoulBlade") },
|
||||
{ "BossSkillSO", ("SKL_", "SKL_{Name},例:SKL_BossRage") },
|
||||
{ "SkillSequenceSO", ("SKL_", "SKL_Seq_{Name},例:SKL_Seq_RageCombo") },
|
||||
{ "EnemyStatsSO", ("ENM_", "ENM_E{ID}_Stats,例:ENM_E001_Stats") },
|
||||
{ "LootTableSO", ("ENM_", "ENM_E{ID}_Loot,例:ENM_E001_Loot") },
|
||||
{ "FormConfigSO", ("PLY_", "PLY_{FormID},例:PLY_Player01") },
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个攻击图案的数据。伤害参数只写在此处,BossSkillSO 不存参数。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Boss/AttackPattern")]
|
||||
public class AttackPatternSO : ScriptableObject
|
||||
{
|
||||
[Header("输出")]
|
||||
public DamageSourceSO DamageSource;
|
||||
public float KnockbackAngle;
|
||||
|
||||
[Header("弹幕(若为弹幕类型)")]
|
||||
public AssetReferenceGameObject ProjectilePrefab;
|
||||
public int ProjectileCount = 1;
|
||||
public float SpreadAngle = 0f;
|
||||
public float ProjectileSpeed = 8f;
|
||||
|
||||
[Header("范围攻击(若为 AoE 类型)")]
|
||||
public float AoERadius;
|
||||
public Vector2 AoEOffset;
|
||||
|
||||
[Header("时序")]
|
||||
[Min(0f)] public float WindupDuration;
|
||||
[Min(0f)] public float ActiveDuration;
|
||||
[Min(0f)] public float RecoveryDuration;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81f89b6e2f8f2774ab7cedbe45dcb810
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,359 +0,0 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Animancer;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// 挂在 Boss GameObject 上,接收 BossOrchestrator 的指令执行指定 BossSkillSO。
|
||||
/// 管理 VulnerabilityWindow 计时和 WeakPointSystem 激活。
|
||||
/// </summary>
|
||||
public class BossSkillExecutor : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private HitBox[] _hitBoxes;
|
||||
[SerializeField] private WeakPointSystem _weakPointSystem;
|
||||
[SerializeField] private AnimancerComponent _animancer;
|
||||
[SerializeField] private string _bossId;
|
||||
[SerializeField] private BossSkillEventChannelSO _onBossSkillStarted;
|
||||
[SerializeField] private BossSkillEventChannelSO _onBossSkillEnded;
|
||||
/// <remarks>PlayerController 无 Instance(架构 05 §2),由 Inspector 指定。</remarks>
|
||||
[SerializeField] private Transform _playerTransform;
|
||||
|
||||
[SerializeField] private BossSkillSO[] _skills;
|
||||
|
||||
[Header("技能重复检测范围")]
|
||||
[Tooltip("SkillSequence RepeatIfPlayerInRange 的检测半径(m)")]
|
||||
[SerializeField, Min(1f)] private float _repeatRangeCheck = 8f;
|
||||
|
||||
private BossSkillSO _currentSkill;
|
||||
private bool _isExecuting;
|
||||
private Coroutine _activeCoroutine;
|
||||
private Coroutine _vulnCoroutine; // 弱点窗口协程(中断时需同步停止)
|
||||
private bool _patternHitConfirmed; // 本次技能执行期间是否有 HitBox 命中
|
||||
|
||||
// 技能冷却:skillId → 冷却结束的 Time.time 时刻
|
||||
private readonly Dictionary<string, float> _skillCooldownEndTimes = new();
|
||||
|
||||
public bool IsExecuting => _isExecuting;
|
||||
|
||||
/// <summary>检查指定技能是否冷却就绪(无冷却记录或已过冷却时间)。</summary>
|
||||
public bool CanUseSkill(string skillId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(skillId)) return false;
|
||||
if (_skillCooldownEndTimes.TryGetValue(skillId, out float endTime))
|
||||
return Time.time >= endTime;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>强制重置指定技能的冷却(阶段切换、复活等场景使用)。</summary>
|
||||
public void ResetSkillCooldown(string skillId)
|
||||
{
|
||||
_skillCooldownEndTimes.Remove(skillId);
|
||||
}
|
||||
|
||||
/// <summary>重置所有技能冷却。</summary>
|
||||
public void ResetAllCooldowns() => _skillCooldownEndTimes.Clear();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
ValidateSkillConfig();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate() => ValidateSkillConfig();
|
||||
|
||||
private void ValidateSkillConfig()
|
||||
{
|
||||
if (_skills == null || _skills.Length == 0)
|
||||
{
|
||||
Debug.LogError($"[BossSkillExecutor] Boss '{_bossId}' ({gameObject.name}) 未配置任何技能 SO。", this);
|
||||
return;
|
||||
}
|
||||
foreach (var skill in _skills)
|
||||
{
|
||||
if (skill == null)
|
||||
Debug.LogError($"[BossSkillExecutor] Boss '{_bossId}' ({gameObject.name}) _skills 数组含 null 元素。", this);
|
||||
else if (string.IsNullOrEmpty(skill.skillId))
|
||||
Debug.LogError($"[BossSkillExecutor] Boss '{_bossId}' ({gameObject.name}) 技能 '{skill.name}' 缺少 skillId。", this);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// 按 float 值复用 WaitForSeconds 实例,消除协程中每次 new WaitForSeconds 的 GC 分配。
|
||||
/// Domain Reload 禁用时静态缓存跨 PlayMode 会话保留,但 WaitForSeconds 是幂等值对象,
|
||||
/// 不会引发功能错误;[RuntimeInitializeOnLoadMethod] 确保每次进入 Play 时清空。
|
||||
/// </summary>
|
||||
private static readonly Dictionary<float, WaitForSeconds> _wfsCache = new();
|
||||
|
||||
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ClearWFSCache() => _wfsCache.Clear();
|
||||
|
||||
private const int MaxWFSCacheSize = 64;
|
||||
|
||||
private static WaitForSeconds GetWFS(float t)
|
||||
{
|
||||
if (!_wfsCache.TryGetValue(t, out var wfs))
|
||||
{
|
||||
if (_wfsCache.Count < MaxWFSCacheSize)
|
||||
_wfsCache[t] = wfs = new WaitForSeconds(t);
|
||||
else
|
||||
return new WaitForSeconds(t);
|
||||
}
|
||||
return wfs;
|
||||
}
|
||||
|
||||
// ── 公共 API ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 按 skillId 查找已在 Inspector 注册的技能 SO。未找到返回 null。
|
||||
/// </summary>
|
||||
public BossSkillSO FindSkill(string skillId)
|
||||
{
|
||||
if (_skills == null) return null;
|
||||
foreach (var s in _skills)
|
||||
if (s != null && s.skillId == skillId) return s;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>返回当前正在执行的技能 SO,未执行时返回 null。</summary>
|
||||
public BossSkillSO FindCurrentSkill() => _isExecuting ? _currentSkill : null;
|
||||
|
||||
/// <summary>Inspector 中注册的全部技能 SO(只读)。</summary>
|
||||
public BossSkillSO[] Skills => _skills;
|
||||
|
||||
/// <summary>
|
||||
/// 执行一个 Boss 技能。若当前正在执行或技能冷却未就绪则返回。
|
||||
/// </summary>
|
||||
public void ExecuteSkill(BossSkillSO skill)
|
||||
{
|
||||
if (_isExecuting || skill == null) return;
|
||||
if (!CanUseSkill(skill.skillId))
|
||||
{
|
||||
Debug.Log($"[BossSkillExecutor] 技能 '{skill.skillId}' 冷却中,无法执行。", this);
|
||||
return;
|
||||
}
|
||||
// 提前订阅,确保 InterruptCurrentSkill() 中断时 FinishExecution() 能正常取消
|
||||
SubscribeHitCallbacks();
|
||||
_activeCoroutine = StartCoroutine(ExecuteSkillCoroutine(skill));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 立即打断正在执行的技能(阶段切换时调用)。
|
||||
/// </summary>
|
||||
public void InterruptCurrentSkill()
|
||||
{
|
||||
// 同步停止弱点窗口协程,防止中断后继续激活 WeakPointSystem
|
||||
if (_vulnCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_vulnCoroutine);
|
||||
_vulnCoroutine = null;
|
||||
}
|
||||
if (_activeCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_activeCoroutine);
|
||||
_activeCoroutine = null;
|
||||
}
|
||||
FinishExecution();
|
||||
}
|
||||
|
||||
// 等待事件触发的 VulnWindow(事件驱动类型,存储后由 NotifyVulnTrigger 逐个激活)
|
||||
private readonly List<VulnerabilityWindow> _pendingEventWindows = new();
|
||||
|
||||
/// <summary>
|
||||
/// 通知执行器某一外部事件已发生(如格挡成功、反制命中等),
|
||||
/// 激活所有注册该触发类型的弱点窗口。
|
||||
/// 由 BossBase.HandleParrySuccess / ApplyCounterResponse 等调用。
|
||||
/// </summary>
|
||||
public void NotifyVulnTrigger(VulnTriggerType triggerType)
|
||||
{
|
||||
for (int i = _pendingEventWindows.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var w = _pendingEventWindows[i];
|
||||
if (w.TriggerType == triggerType)
|
||||
{
|
||||
_pendingEventWindows.RemoveAt(i);
|
||||
StartCoroutine(OpenWindowCoroutine(w));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHitConfirmedCallback(DamageInfo _) => _patternHitConfirmed = true;
|
||||
|
||||
private void SubscribeHitCallbacks()
|
||||
{
|
||||
if (_hitBoxes == null) return;
|
||||
foreach (var hb in _hitBoxes) if (hb != null) hb.OnHitConfirmed += OnHitConfirmedCallback;
|
||||
}
|
||||
|
||||
private void UnsubscribeHitCallbacks()
|
||||
{
|
||||
if (_hitBoxes == null) return;
|
||||
foreach (var hb in _hitBoxes) if (hb != null) hb.OnHitConfirmed -= OnHitConfirmedCallback;
|
||||
}
|
||||
|
||||
private IEnumerator ExecuteSkillCoroutine(BossSkillSO skill)
|
||||
{
|
||||
_isExecuting = true;
|
||||
_currentSkill = skill;
|
||||
_patternHitConfirmed = false;
|
||||
|
||||
// HitBox 订阅已在 ExecuteSkill() 入口完成(确保 Interrupt 中断时能在 FinishExecution 取消)
|
||||
|
||||
_onBossSkillStarted?.Raise(new BossSkillEvent { BossId = _bossId, SkillId = skill.skillId });
|
||||
|
||||
// 播放技能动画
|
||||
if (skill.skillAnimation != null)
|
||||
_animancer.Play(skill.skillAnimation);
|
||||
|
||||
// 启动 VulnerabilityWindow 协程(与主序列并行)
|
||||
_vulnCoroutine = null;
|
||||
if (skill.vulnerabilityWindows != null && skill.vulnerabilityWindows.Length > 0)
|
||||
_vulnCoroutine = StartCoroutine(ActivateVulnerabilityWindowsCoroutine(skill));
|
||||
|
||||
// 执行主攻击序列(始终执行 sequenceOnMiss;sequenceOnHit 是命中后的追加序列)
|
||||
if (skill.sequenceOnMiss != null)
|
||||
yield return ExecuteSequenceCoroutine(skill.sequenceOnMiss);
|
||||
|
||||
// 若本次有命中确认且配置了 sequenceOnHit,执行追加序列(连段、击倒追击等)
|
||||
if (_patternHitConfirmed && skill.sequenceOnHit != null)
|
||||
yield return ExecuteSequenceCoroutine(skill.sequenceOnHit);
|
||||
|
||||
// 若弱点协程还在运行则等待其结束(避免孤立协程)
|
||||
if (_vulnCoroutine != null)
|
||||
yield return _vulnCoroutine;
|
||||
|
||||
FinishExecution();
|
||||
}
|
||||
|
||||
private void FinishExecution()
|
||||
{
|
||||
UnsubscribeHitCallbacks(); // 无论正常结束还是被 Interrupt,均在此取消订阅
|
||||
_pendingEventWindows.Clear(); // 清除未触发的事件驱动弱点窗口,防止跨技能积压
|
||||
_vulnCoroutine = null; // 正常结束时已自然结束,仅清除引用
|
||||
_isExecuting = false;
|
||||
if (_currentSkill != null)
|
||||
{
|
||||
// 记录冷却结束时刻
|
||||
if (_currentSkill.cooldown > 0f)
|
||||
_skillCooldownEndTimes[_currentSkill.skillId] = Time.time + _currentSkill.cooldown;
|
||||
|
||||
_onBossSkillEnded?.Raise(new BossSkillEvent { BossId = _bossId, SkillId = _currentSkill.skillId });
|
||||
_currentSkill = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 序列协程 ────────────────────────────────────────────────────────────
|
||||
|
||||
private IEnumerator ExecuteSequenceCoroutine(SkillSequenceSO seq)
|
||||
{
|
||||
int repeatCount = 0;
|
||||
do
|
||||
{
|
||||
foreach (var step in seq.steps)
|
||||
{
|
||||
if (step.delayBeforeStep > 0f)
|
||||
yield return GetWFS(step.delayBeforeStep);
|
||||
|
||||
if (step.pattern != null)
|
||||
yield return ExecutePatternCoroutine(step.pattern);
|
||||
}
|
||||
|
||||
repeatCount++;
|
||||
|
||||
if (seq.RepeatIfPlayerInRange && seq.RepeatDelay > 0f)
|
||||
yield return GetWFS(seq.RepeatDelay);
|
||||
}
|
||||
while (seq.RepeatIfPlayerInRange
|
||||
&& (seq.MaxRepeatCount == 0 || repeatCount < seq.MaxRepeatCount)
|
||||
&& IsPlayerInRange());
|
||||
}
|
||||
|
||||
private IEnumerator ExecutePatternCoroutine(AttackPatternSO pattern)
|
||||
{
|
||||
// 预备
|
||||
if (pattern.WindupDuration > 0f)
|
||||
yield return GetWFS(pattern.WindupDuration);
|
||||
|
||||
// 激活 HitBox(架构 06 §4:Activate(DamageSourceSO, Transform))
|
||||
if (_hitBoxes != null && _hitBoxes.Length > 0)
|
||||
foreach (var hb in _hitBoxes)
|
||||
if (hb != null) hb.Activate(pattern.DamageSource, transform);
|
||||
|
||||
if (pattern.ActiveDuration > 0f)
|
||||
yield return GetWFS(pattern.ActiveDuration);
|
||||
|
||||
// 关闭 HitBox
|
||||
if (_hitBoxes != null && _hitBoxes.Length > 0)
|
||||
foreach (var hb in _hitBoxes)
|
||||
if (hb != null) hb.Deactivate();
|
||||
|
||||
// 后摇
|
||||
if (pattern.RecoveryDuration > 0f)
|
||||
yield return GetWFS(pattern.RecoveryDuration);
|
||||
}
|
||||
|
||||
// ── VulnerabilityWindow 协程 ─────────────────────────────────────────────
|
||||
|
||||
private IEnumerator ActivateVulnerabilityWindowsCoroutine(BossSkillSO skill)
|
||||
{
|
||||
_pendingEventWindows.Clear();
|
||||
|
||||
foreach (var window in skill.vulnerabilityWindows)
|
||||
{
|
||||
if (window.TriggerType == VulnTriggerType.OnAttackRecovery)
|
||||
{
|
||||
// 时间驱动:按 TriggerDelay 延迟后自动激活
|
||||
if (window.TriggerDelay > 0f)
|
||||
yield return GetWFS(window.TriggerDelay);
|
||||
StartCoroutine(OpenWindowCoroutine(window));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 事件驱动(OnParriedSuccess / Manual 等):注册到待触发列表
|
||||
_pendingEventWindows.Add(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>实际开启并持续弱点窗口,支持独立并行运行。</summary>
|
||||
private IEnumerator OpenWindowCoroutine(VulnerabilityWindow window)
|
||||
{
|
||||
bool activateSpecific = window.ActivateWeakPointHurtBox;
|
||||
_weakPointSystem?.SetActive(true, window.DamageMultiplier, activateSpecific);
|
||||
window.OpenFeedback?.Play();
|
||||
|
||||
yield return GetWFS(window.Duration);
|
||||
|
||||
_weakPointSystem?.SetActive(false, 1f, activateSpecific);
|
||||
window.CloseFeedback?.Play();
|
||||
}
|
||||
|
||||
// ── 工具 ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 在指定时长内开启弱点窗口(格挡/闪避反制时调用,独立于技能 VulnerabilityWindow 序列)。
|
||||
/// </summary>
|
||||
public void OpenVulnerabilityWindow(float duration, float damageMultiplier)
|
||||
{
|
||||
if (_weakPointSystem == null || duration <= 0f) return;
|
||||
StartCoroutine(VulnWindowOverride(duration, damageMultiplier));
|
||||
}
|
||||
|
||||
private IEnumerator VulnWindowOverride(float duration, float multiplier)
|
||||
{
|
||||
_weakPointSystem.SetActive(true, multiplier, false);
|
||||
yield return GetWFS(duration);
|
||||
_weakPointSystem.SetActive(false, 1f, false);
|
||||
}
|
||||
|
||||
private bool IsPlayerInRange() =>
|
||||
_playerTransform != null &&
|
||||
Vector2.Distance(transform.position, _playerTransform.position) < _repeatRangeCheck;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4dfa1c525eaca5640b3cfe945626a466
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,69 +0,0 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Animancer;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// Boss 单个技能的所有数据,包括攻击模式、弱点窗口、互动标签等。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Boss/BossSkill")]
|
||||
public class BossSkillSO : ScriptableObject
|
||||
{
|
||||
[Header("元信息")]
|
||||
[Tooltip("技能唯一标识符(全小写英文 + 下划线,如 'slash_combo'、'phase_dash')。BD_UseBossSkill 通过此 Id 引用")]
|
||||
public string skillId;
|
||||
[Tooltip("编辑器中显示的可读名称,不影响运行逻辑")]
|
||||
public string displayName;
|
||||
[TextArea(1, 4)]
|
||||
[Tooltip("设计备注(仅供编辑器参考,不影响运行)")]
|
||||
public string designNote;
|
||||
|
||||
[Header("技能分类")]
|
||||
public BossSkillCategory category;
|
||||
public BossSkillType skillType;
|
||||
|
||||
[Header("阶段可用性")]
|
||||
[Tooltip("空数组 = 全阶段可用")]
|
||||
public int[] availablePhaseIndices;
|
||||
|
||||
[Header("核心攻击动作引用")]
|
||||
public AttackPatternSO[] attackPatterns;
|
||||
|
||||
[Header("弱点窗口(至少 1 个)")]
|
||||
public VulnerabilityWindow[] vulnerabilityWindows;
|
||||
|
||||
[Header("互动标签")]
|
||||
public InteractionTag interactionTags;
|
||||
|
||||
[Header("连段")]
|
||||
public SkillSequenceSO sequenceOnHit;
|
||||
public SkillSequenceSO sequenceOnMiss;
|
||||
|
||||
[Header("玩家反制接口")]
|
||||
public PlayerCounterResponse[] counterResponses;
|
||||
|
||||
[Header("场景联动")]
|
||||
public ArenaEventTrigger[] arenaEvents;
|
||||
|
||||
[Header("Boss 资源")]
|
||||
public BossResourceCost resourceCost;
|
||||
public bool buildsRage;
|
||||
|
||||
[Header("霸体配置")]
|
||||
public PoiseWindowConfig poiseWindow;
|
||||
|
||||
[Header("动画")]
|
||||
public ClipTransition skillAnimation;
|
||||
|
||||
[Header("冷却")]
|
||||
[Min(0f)]
|
||||
public float cooldown;
|
||||
|
||||
[Header("权重随机(UseBossSkillWeighted 使用)")]
|
||||
[Tooltip("相对权重,数值越大被随机选中的概率越高;0 = 禁用随机选择")]
|
||||
[Min(0f)]
|
||||
public float weight = 1f;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de92221c7c3fb4a42a7cd122a8f97632
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,184 +0,0 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using BaseGames.Feedback;
|
||||
|
||||
namespace BaseGames.Boss
|
||||
{
|
||||
// ── 分类枚举 ───────────────────────────────────────────────────────────────
|
||||
|
||||
public enum BossSkillCategory
|
||||
{
|
||||
Melee, Ranged, Charge, AoE, Environmental, Summon,
|
||||
Buff, Debuff, Phase, Passive, Reactive
|
||||
}
|
||||
|
||||
public enum BossSkillType
|
||||
{
|
||||
MeleeSlash,
|
||||
ChargeAttack,
|
||||
LeapSlam,
|
||||
ProjectileVolley,
|
||||
LaserBeam,
|
||||
PhaseTransition,
|
||||
SummonMinion,
|
||||
ArenaTrap,
|
||||
SpeedBurst,
|
||||
DefensiveShell,
|
||||
}
|
||||
|
||||
[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,
|
||||
}
|
||||
|
||||
// ── VulnerabilityWindow ────────────────────────────────────────────────────
|
||||
|
||||
public enum VulnTriggerType
|
||||
{
|
||||
OnAttackRecovery,
|
||||
OnParriedSuccess,
|
||||
OnCounterSkillHit,
|
||||
OnPhaseTransition,
|
||||
OnHazardBackfire,
|
||||
OnSummonDefeated,
|
||||
Manual,
|
||||
}
|
||||
|
||||
public enum WeakPointType
|
||||
{
|
||||
FullBody,
|
||||
HeadOnly,
|
||||
BackOnly,
|
||||
CoreExposed,
|
||||
CustomPoint,
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct VulnerabilityWindow
|
||||
{
|
||||
[Tooltip("弱点触发方式")]
|
||||
public VulnTriggerType TriggerType;
|
||||
|
||||
[Tooltip("触发后延迟出现(秒)")]
|
||||
[Min(0f)]
|
||||
public float TriggerDelay;
|
||||
|
||||
[Tooltip("弱点持续时长(秒)")]
|
||||
[Min(0.1f)]
|
||||
public float Duration;
|
||||
|
||||
public WeakPointType WeakPointType;
|
||||
|
||||
[Tooltip("弱点激活时 Boss 受击乘数")]
|
||||
[Min(0.1f)]
|
||||
public float DamageMultiplier;
|
||||
|
||||
public bool ForceStagger;
|
||||
|
||||
[Min(0f)]
|
||||
public float StaggerDuration;
|
||||
|
||||
public SceneFeedback OpenFeedback;
|
||||
public SceneFeedback CloseFeedback;
|
||||
public Color HighlightColor;
|
||||
|
||||
public bool ActivateWeakPointHurtBox => WeakPointType != WeakPointType.FullBody;
|
||||
}
|
||||
|
||||
// ── PlayerCounterResponse ─────────────────────────────────────────────────
|
||||
|
||||
public enum CounterType
|
||||
{
|
||||
Parry,
|
||||
PerfectParry,
|
||||
DodgeThrough,
|
||||
SpecificSkill,
|
||||
WeakPointHit,
|
||||
HazardBackfire,
|
||||
SummonKill,
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct PlayerCounterResponse
|
||||
{
|
||||
[Header("反制条件")]
|
||||
public CounterType counterType;
|
||||
public string requiredSkillId;
|
||||
|
||||
[Header("反制效果(对 Boss)")]
|
||||
public float bossStaggerDuration;
|
||||
public float bossDamageBonus;
|
||||
public bool openVulnWindow;
|
||||
public bool interruptSkill;
|
||||
|
||||
[Header("反制收益(对玩家)")]
|
||||
public int soulPowerGrant;
|
||||
public int spiritPowerGrant;
|
||||
public SceneFeedback counterFeedback;
|
||||
}
|
||||
|
||||
// ── ArenaEvent ────────────────────────────────────────────────────────────
|
||||
|
||||
public enum ArenaEventType
|
||||
{
|
||||
DestroyPlatform,
|
||||
ActivateHazard,
|
||||
DeactivateHazard,
|
||||
SpawnHazardArea,
|
||||
ShakeArena,
|
||||
ToggleLighting,
|
||||
SpawnPlatform,
|
||||
TriggerCutscene,
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct ArenaEventParams
|
||||
{
|
||||
public float duration;
|
||||
public float intensity;
|
||||
public bool revertsOnPhaseEnd;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct ArenaEventTrigger
|
||||
{
|
||||
public string targetArenaObjectId;
|
||||
public ArenaEventType eventType;
|
||||
public float delay;
|
||||
public ArenaEventParams parameters;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct ArenaEventData
|
||||
{
|
||||
public ArenaEventType type;
|
||||
public ArenaEventParams parameters;
|
||||
public string sourceSkillId;
|
||||
}
|
||||
|
||||
public interface IArenaInteractable
|
||||
{
|
||||
string ArenaObjectId { get; }
|
||||
void OnBossArenaEvent(ArenaEventData data);
|
||||
}
|
||||
|
||||
// ── BossResourceCost ──────────────────────────────────────────────────────
|
||||
|
||||
[Serializable]
|
||||
public struct BossResourceCost
|
||||
{
|
||||
public string resourceId;
|
||||
public float cost;
|
||||
public float minRequired;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1beb8f8f7958b84c9ab60abe5f8c4ed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -2,6 +2,7 @@ using System.Collections;
|
||||
using Animancer;
|
||||
using BaseGames.Boss;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Enemies.Abilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.Boss
|
||||
@@ -79,7 +80,8 @@ namespace BaseGames.Enemies.Boss
|
||||
{
|
||||
_inKnockdown = true;
|
||||
|
||||
_boss.GetComponentInChildren<BossSkillExecutor>()?.InterruptCurrentSkill();
|
||||
// 击落打断当前正在执行的所有能力(招式统一由能力注册表管理)
|
||||
_boss.Abilities.InterruptAll(InterruptReason.ExternalRequest);
|
||||
|
||||
if (_knockdownHitClip.Clip != null)
|
||||
_boss.Animancer.Play(_knockdownHitClip);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d8f5f23ee1dde046b1a7361ac1b6386
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.Enemies.Boss.Patterns",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.Enemies.Boss.Patterns",
|
||||
"references": [
|
||||
"BaseGames.Core",
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Enemies",
|
||||
"BaseGames.Combat"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8bc3529e552a34a45998814c7cd056e6
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,50 +0,0 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Pool;
|
||||
|
||||
namespace BaseGames.Enemies.Boss.Patterns
|
||||
{
|
||||
/// <summary>
|
||||
/// 攻击预警系统(架构 07_EnemyModule §11)。
|
||||
/// 在攻击前若干帧显示视觉提示(VFX 从对象池取出,到期归还)。
|
||||
/// 由 BD_TelegraphAttack 通过协程调用 ShowTelegraph。
|
||||
/// </summary>
|
||||
public class TelegraphSystem : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// 开始预警:从对象池取出 vfxKey 对应预警 VFX,等待 duration 秒后归还。
|
||||
/// 由 BD_TelegraphAttack.OnStart 通过 StartCoroutine 调用。
|
||||
/// </summary>
|
||||
public IEnumerator ShowTelegraph(string vfxKey, float duration, Vector2 position)
|
||||
{
|
||||
if (string.IsNullOrEmpty(vfxKey) || duration <= 0f)
|
||||
{
|
||||
yield return null;
|
||||
yield break;
|
||||
}
|
||||
|
||||
GameObject vfx = null;
|
||||
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
|
||||
if (pool != null)
|
||||
vfx = pool.Spawn(vfxKey, new Vector3(position.x, position.y, 0f), Quaternion.identity);
|
||||
else
|
||||
Debug.LogWarning($"[TelegraphSystem] IObjectPoolService 未就绪,预警 VFX '{vfxKey}' 无法显示。");
|
||||
|
||||
yield return new WaitForSeconds(duration);
|
||||
|
||||
if (vfx != null && pool != null)
|
||||
{
|
||||
var po = vfx.GetComponent<PooledObject>();
|
||||
if (po != null) po.ReturnToPool();
|
||||
else vfx.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>立即隐藏所有活跃预警 VFX(技能被打断时调用)。</summary>
|
||||
public void CancelTelegraph()
|
||||
{
|
||||
StopAllCoroutines();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6f4987894dfe1648909b6863c003c31
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,26 +0,0 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// 有序攻击序列(一个技能内的多段连段)。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Boss/SkillSequence")]
|
||||
public class SkillSequenceSO : ScriptableObject
|
||||
{
|
||||
[Serializable]
|
||||
public struct SequenceStep
|
||||
{
|
||||
public AttackPatternSO pattern;
|
||||
[Min(0f)] public float delayBeforeStep;
|
||||
}
|
||||
|
||||
public SequenceStep[] steps;
|
||||
|
||||
[Header("序列完成后的行为")]
|
||||
public bool RepeatIfPlayerInRange;
|
||||
[Min(0f)] public float RepeatDelay;
|
||||
[Range(0, 10)] public int MaxRepeatCount;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab2ec01e225283d4face08cef0d72c87
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,51 +0,0 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// 管理 Boss 的专属弱点 HurtBox(如核心、眼睛等)。
|
||||
/// 弱点激活期间受到的伤害会乘以 DamageMultiplier。
|
||||
/// </summary>
|
||||
public class WeakPointSystem : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct WeakPoint
|
||||
{
|
||||
public HurtBox hurtBox;
|
||||
public GameObject visualIndicator;
|
||||
}
|
||||
|
||||
[SerializeField] private WeakPoint[] _weakPoints;
|
||||
[SerializeField] private string _bossId;
|
||||
[SerializeField] private StringEventChannelSO _onVulnerabilityWindowOpened;
|
||||
|
||||
private float _damageMultiplier = 1f;
|
||||
|
||||
/// <summary>激活或关闭弱点 HurtBox 及视觉指示器。</summary>
|
||||
/// <param name="active">是否激活。</param>
|
||||
/// <param name="multiplier">激活时的受击伤害乘数。</param>
|
||||
/// <param name="activateSpecific">true = 仅激活弱点专属 HurtBox;false = 全身视为弱点(不改变 HurtBox 状态)。</param>
|
||||
public void SetActive(bool active, float multiplier = 1f, bool activateSpecific = false)
|
||||
{
|
||||
_damageMultiplier = active ? multiplier : 1f;
|
||||
|
||||
if (activateSpecific)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96ffe91642a6ccc4ea4c6076d80f5e27
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user