多轮审查和修复
This commit is contained in:
20
Assets/Scripts/Skills/BaseGames.Skills.asmdef
Normal file
20
Assets/Scripts/Skills/BaseGames.Skills.asmdef
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.Skills",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.Skills",
|
||||
"references": [
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Player",
|
||||
"BaseGames.Input",
|
||||
"BaseGames.Combat",
|
||||
"Kybernetik.Animancer"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
7
Assets/Scripts/Skills/BaseGames.Skills.asmdef.meta
Normal file
7
Assets/Scripts/Skills/BaseGames.Skills.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6aafac5a7a203441bfb350aac033a04
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
85
Assets/Scripts/Skills/FormSkillSO.cs
Normal file
85
Assets/Scripts/Skills/FormSkillSO.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using UnityEngine;
|
||||
using Animancer;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// 技能资源消耗类型。
|
||||
/// </summary>
|
||||
public enum SkillResourceType { SoulPower, SpiritPower }
|
||||
|
||||
/// <summary>
|
||||
/// 技能效果类型(决定施放时执行的逻辑分支)。
|
||||
/// </summary>
|
||||
public enum SkillEffectType
|
||||
{
|
||||
MeleeAoE,
|
||||
Projectile,
|
||||
BarrierAura,
|
||||
GroundDive,
|
||||
DragonKick,
|
||||
WraithDash,
|
||||
ShadowDecoy,
|
||||
DelayedExplosion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反馈预设 SO(FeedbackPresetSO),封装一组 MMF_Player 反馈配置。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Skills/FeedbackPreset")]
|
||||
public class FeedbackPresetSO : ScriptableObject { }
|
||||
|
||||
/// <summary>
|
||||
/// 形态技能数据 SO(架构 09_ProgressionModule §8)。
|
||||
/// 路径: Assets/Scripts/Skills/FormSkillSO.cs
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Skills/FormSkill")]
|
||||
public class FormSkillSO : ScriptableObject
|
||||
{
|
||||
[Header("Identity")]
|
||||
public string skillId;
|
||||
public string displayNameKey;
|
||||
[TextArea(1, 3)]
|
||||
public string descriptionKey;
|
||||
public Sprite icon;
|
||||
|
||||
[Header("Resource")]
|
||||
public SkillResourceType resourceType;
|
||||
public int baseCost;
|
||||
public float cooldown;
|
||||
|
||||
[Header("Animation")]
|
||||
public ClipTransition castAnimation;
|
||||
public float castLockDuration; // 施放锁定时长(秒)
|
||||
|
||||
[Header("Effect")]
|
||||
public SkillEffectType effectType;
|
||||
public DamageSourceSO damageSource;
|
||||
|
||||
[Header("Projectile")]
|
||||
public ProjectileConfigSO projectileConfig;
|
||||
public bool isHoming;
|
||||
public bool holdForContinuous;
|
||||
|
||||
[Header("Dash")]
|
||||
public float dashForce;
|
||||
public float dashDuration;
|
||||
public bool isInvincibleDuringDash;
|
||||
|
||||
[Header("Explosion")]
|
||||
public float explosionDelay;
|
||||
public float explosionRadius;
|
||||
|
||||
[Header("Feedback")]
|
||||
public FeedbackPresetSO castFeedback;
|
||||
|
||||
[Header("HitBox Prefab")]
|
||||
/// <summary>
|
||||
/// 近战/爆炸技能的命中盒 Prefab,内含 SkillHitBoxInstance + HitBox。
|
||||
/// 投射物技能此字段留空——由 ProjectileConfigSO 负责。
|
||||
/// 命名规范: Assets/Prefabs/Skills/SKL_{skillId}_HitBox.prefab
|
||||
/// </summary>
|
||||
public GameObject SkillHitBoxPrefab;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Skills/FormSkillSO.cs.meta
Normal file
11
Assets/Scripts/Skills/FormSkillSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a96f0270221c8444b8719f0f9b14c635
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
135
Assets/Scripts/Skills/SkillManager.cs
Normal file
135
Assets/Scripts/Skills/SkillManager.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using UnityEngine;
|
||||
using Animancer;
|
||||
using System.Collections.Generic;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// 技能管理器(架构 09_ProgressionModule §9)。
|
||||
/// 挂在 Player 上,订阅输入事件并处理所有技能施放逻辑。
|
||||
/// 通过 UpdateSkillSet() 由 FormController 注入当前形态技能。
|
||||
/// 直接使用 AnimancerComponent 播放动画,无需 SoulSkillState FSM。
|
||||
/// </summary>
|
||||
public class SkillManager : MonoBehaviour
|
||||
{
|
||||
[Header("依赖引用")]
|
||||
[SerializeField] private PlayerStats _stats;
|
||||
[SerializeField] private AnimancerComponent _animancer;
|
||||
[SerializeField] private InputReaderSO _input;
|
||||
[SerializeField] private SkillModifierRegistry _modifiers;
|
||||
|
||||
[Header("技能挂载点")]
|
||||
[SerializeField] private Transform _skillSocket; // [SkillSocket] 子节点
|
||||
|
||||
// 当前形态技能集(绑定到对应输入槽)
|
||||
private FormSkillSO _soulSkill;
|
||||
private FormSkillSO _spirit1;
|
||||
private FormSkillSO _spirit2;
|
||||
|
||||
// 冷却字典(FormSkillSO → 剩余冷却秒数),UpdateSkillSet 时重建
|
||||
private readonly Dictionary<FormSkillSO, float> _cooldowns = new(3);
|
||||
// 无分配 Update 遍历用的快照数组
|
||||
private FormSkillSO[] _activeSkills = System.Array.Empty<FormSkillSO>();
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_input == null) return;
|
||||
_input.SoulSkillEvent += TrySoulSkill;
|
||||
_input.SpiritSkill1StartedEvent += TrySpiritSkill1;
|
||||
_input.SpiritSkill2StartedEvent += TrySpiritSkill2;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_input == null) return;
|
||||
_input.SoulSkillEvent -= TrySoulSkill;
|
||||
_input.SpiritSkill1StartedEvent -= TrySpiritSkill1;
|
||||
_input.SpiritSkill2StartedEvent -= TrySpiritSkill2;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
for (int i = 0; i < _activeSkills.Length; i++)
|
||||
{
|
||||
var s = _activeSkills[i];
|
||||
if (_cooldowns.TryGetValue(s, out float cd) && cd > 0f)
|
||||
_cooldowns[s] = cd - Time.deltaTime;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 公共 API ─────────────────────────────────────────────────────────
|
||||
/// <summary>切换形态时由 FormController 调用,注入当前形态的三个技能。</summary>
|
||||
public void UpdateSkillSet(FormSkillSO soul, FormSkillSO spirit1, FormSkillSO spirit2)
|
||||
{
|
||||
_soulSkill = soul;
|
||||
_spirit1 = spirit1;
|
||||
_spirit2 = spirit2;
|
||||
|
||||
_cooldowns.Clear();
|
||||
// 构建无分配遍历快照(固定大小数组,避免 List + ToArray GC)
|
||||
int count = (soul != null ? 1 : 0) + (spirit1 != null ? 1 : 0) + (spirit2 != null ? 1 : 0);
|
||||
if (_activeSkills.Length != count)
|
||||
_activeSkills = count > 0 ? new FormSkillSO[count] : System.Array.Empty<FormSkillSO>();
|
||||
int idx = 0;
|
||||
if (soul != null) { _cooldowns[soul] = 0f; _activeSkills[idx++] = soul; }
|
||||
if (spirit1 != null) { _cooldowns[spirit1] = 0f; _activeSkills[idx++] = spirit1; }
|
||||
if (spirit2 != null) { _cooldowns[spirit2] = 0f; _activeSkills[idx] = spirit2; }
|
||||
}
|
||||
|
||||
// ── 内部施放逻辑 ─────────────────────────────────────────────────────
|
||||
private void TrySoulSkill() => TryCastSkill(_soulSkill);
|
||||
private void TrySpiritSkill1() => TryCastSkill(_spirit1);
|
||||
private void TrySpiritSkill2() => TryCastSkill(_spirit2);
|
||||
|
||||
private void TryCastSkill(FormSkillSO skill)
|
||||
{
|
||||
if (skill == null) return;
|
||||
|
||||
var p = _modifiers != null
|
||||
? _modifiers.GetEffectiveParams(skill)
|
||||
: EffectiveSkillParams.FromBase(skill);
|
||||
|
||||
if (!_cooldowns.TryGetValue(skill, out float cooldown) || cooldown > 0f) return;
|
||||
|
||||
// 消耗资源
|
||||
bool consumed = skill.resourceType == SkillResourceType.SoulPower
|
||||
? _stats.ConsumeSoulPower(p.effectiveCost)
|
||||
: _stats.ConsumeSpiritPower(p.effectiveCost);
|
||||
if (!consumed) return;
|
||||
|
||||
_cooldowns[skill] = p.effectiveCooldown;
|
||||
|
||||
// 播放动画(优先修改器动画,回退技能默认动画)
|
||||
var clip = p.effectiveAnimation.Clip != null
|
||||
? p.effectiveAnimation
|
||||
: skill.castAnimation;
|
||||
if (clip.Clip != null && _animancer != null)
|
||||
_animancer.Play(clip);
|
||||
|
||||
// 生成 HitBox Prefab(近战/爆炸类技能)
|
||||
if (skill.SkillHitBoxPrefab != null)
|
||||
{
|
||||
var socket = _skillSocket != null ? _skillSocket : transform;
|
||||
var go = Object.Instantiate(skill.SkillHitBoxPrefab, socket.position,
|
||||
socket.rotation, socket);
|
||||
var inst = go.GetComponent<SkillHitBoxInstance>();
|
||||
inst?.Activate(skill.damageSource, transform);
|
||||
inst?.AutoDestroyAfter(skill.castLockDuration > 0f ? skill.castLockDuration : 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 属性查询 ─────────────────────────────────────────────────────────
|
||||
public FormSkillSO SoulSkill => _soulSkill;
|
||||
public FormSkillSO Spirit1 => _spirit1;
|
||||
public FormSkillSO Spirit2 => _spirit2;
|
||||
|
||||
public float SoulCooldownRatio =>
|
||||
(_soulSkill != null && _soulSkill.cooldown > 0 &&
|
||||
_cooldowns.TryGetValue(_soulSkill, out float cd))
|
||||
? Mathf.Clamp01(cd / _soulSkill.cooldown) : 0f;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Skills/SkillManager.cs.meta
Normal file
11
Assets/Scripts/Skills/SkillManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb39b928827b10245aeab5ea41f862a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
167
Assets/Scripts/Skills/SkillModifierRegistry.cs
Normal file
167
Assets/Scripts/Skills/SkillModifierRegistry.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Animancer;
|
||||
using BaseGames.Player;
|
||||
|
||||
namespace BaseGames.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// 技能数值修改维度(架构 09_ProgressionModule §10)。
|
||||
/// </summary>
|
||||
public enum SkillStat { Damage, Cost, Cooldown, Range, Duration }
|
||||
|
||||
/// <summary>
|
||||
/// 插槽覆盖描述符:某护符将指定形态的某个技能槽替换为另一技能。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct SkillSlotOverride
|
||||
{
|
||||
public FormSO targetForm; // null = 所有形态
|
||||
public string targetSlot; // 使用 SkillSlotNames 中的常量(SoulSkill / SpiritSkill1 / SpiritSkill2)
|
||||
public FormSkillSO replacementSkill; // 替换目标技能
|
||||
public int priority; // 高优先级覆盖低优先级
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 所有数值修改器叠加后的运行时参数快照(架构 09_ProgressionModule §10)。
|
||||
/// 由 SkillModifierRegistry.GetEffectiveParams() 生成,传入 SkillManager 使用。
|
||||
/// </summary>
|
||||
public struct EffectiveSkillParams
|
||||
{
|
||||
public FormSkillSO baseSkill; // 原始 SO 引用(不变)
|
||||
public int effectiveCost; // 修改后消耗
|
||||
public float effectiveCooldown; // 修改后冷却(秒)
|
||||
public float damageMult; // 伤害倍率(1.0 = 无增益)
|
||||
public float rangeMult; // 范围倍率
|
||||
public FeedbackPresetSO effectiveFeedback; // 最终特效预设(null = 回退原始)
|
||||
public ClipTransition effectiveAnimation; // 最终施法动画(null = 回退原始)
|
||||
|
||||
/// <summary>以技能 SO 默认值初始化,无任何修改器加成。</summary>
|
||||
public static EffectiveSkillParams FromBase(FormSkillSO skill) => new()
|
||||
{
|
||||
baseSkill = skill,
|
||||
effectiveCost = skill.baseCost,
|
||||
effectiveCooldown = skill.cooldown,
|
||||
damageMult = 1f,
|
||||
rangeMult = 1f,
|
||||
effectiveFeedback = null,
|
||||
effectiveAnimation = default,
|
||||
};
|
||||
}
|
||||
|
||||
// 内部记录结构,存储单条数值修改
|
||||
internal struct SkillStatEntry
|
||||
{
|
||||
public SkillStat stat;
|
||||
public float delta;
|
||||
public bool isPercent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 技能修改器注册表(架构 09_ProgressionModule §10)。
|
||||
/// 挂在 Player 上,收集所有护符对技能数值的修改。
|
||||
/// SkillManager 在施放技能时调用 GetEffectiveParams() 获取最终参数。
|
||||
/// </summary>
|
||||
public class SkillModifierRegistry : MonoBehaviour
|
||||
{
|
||||
// skillId → 一组数值修改
|
||||
private readonly Dictionary<string, List<SkillStatEntry>> _modifiers = new();
|
||||
|
||||
// 插槽覆盖列表(按 priority 降序排列)
|
||||
private readonly List<SkillSlotOverride> _slotOverrides = new();
|
||||
|
||||
// ── 数值修改 ────────────────────────────────────────────────────────
|
||||
public void Register(string skillId, SkillStat stat, float delta, bool isPercent)
|
||||
{
|
||||
if (!_modifiers.TryGetValue(skillId, out var list))
|
||||
{
|
||||
list = new List<SkillStatEntry>();
|
||||
_modifiers[skillId] = list;
|
||||
}
|
||||
list.Add(new SkillStatEntry { stat = stat, delta = delta, isPercent = isPercent });
|
||||
}
|
||||
|
||||
public void Unregister(string skillId, SkillStat stat, float delta, bool isPercent)
|
||||
{
|
||||
if (!_modifiers.TryGetValue(skillId, out var list)) return;
|
||||
list.RemoveAll(e => e.stat == stat &&
|
||||
Mathf.Approximately(e.delta, delta) &&
|
||||
e.isPercent == isPercent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对给定技能叠加所有已注册修改器,返回一次性快照供 SkillManager 使用。
|
||||
/// </summary>
|
||||
public EffectiveSkillParams GetEffectiveParams(FormSkillSO skill)
|
||||
{
|
||||
var p = EffectiveSkillParams.FromBase(skill);
|
||||
if (!_modifiers.TryGetValue(skill.skillId, out var entries)) return p;
|
||||
|
||||
float flatCost = 0, pctCost = 1f;
|
||||
float flatCooldown = 0, pctCooldown = 1f;
|
||||
float flatDamage = 0, pctDamage = 1f;
|
||||
float flatRange = 0, pctRange = 1f;
|
||||
|
||||
foreach (var e in entries)
|
||||
{
|
||||
switch (e.stat)
|
||||
{
|
||||
case SkillStat.Cost:
|
||||
if (e.isPercent) pctCost += e.delta;
|
||||
else flatCost += e.delta;
|
||||
break;
|
||||
case SkillStat.Cooldown:
|
||||
if (e.isPercent) pctCooldown += e.delta;
|
||||
else flatCooldown += e.delta;
|
||||
break;
|
||||
case SkillStat.Damage:
|
||||
if (e.isPercent) pctDamage += e.delta;
|
||||
else flatDamage += e.delta;
|
||||
break;
|
||||
case SkillStat.Range:
|
||||
if (e.isPercent) pctRange += e.delta;
|
||||
else flatRange += e.delta;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
p.effectiveCost = Mathf.Max(0, Mathf.RoundToInt(skill.baseCost * pctCost + flatCost));
|
||||
p.effectiveCooldown = Mathf.Max(0, skill.cooldown * pctCooldown + flatCooldown);
|
||||
p.damageMult = 1f + (pctDamage - 1f) + flatDamage;
|
||||
p.rangeMult = 1f + (pctRange - 1f) + flatRange;
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
// ── 插槽覆盖 ────────────────────────────────────────────────────────
|
||||
public void AddSlotOverride(SkillSlotOverride data)
|
||||
{
|
||||
_slotOverrides.Add(data);
|
||||
_slotOverrides.Sort((a, b) => b.priority.CompareTo(a.priority));
|
||||
}
|
||||
|
||||
public void RemoveSlotOverride(SkillSlotOverride data)
|
||||
{
|
||||
_slotOverrides.RemoveAll(o =>
|
||||
o.targetForm == data.targetForm &&
|
||||
o.targetSlot == data.targetSlot &&
|
||||
o.replacementSkill == data.replacementSkill);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前形态某槽位的实际技能(考虑插槽覆盖)。
|
||||
/// 没有覆盖时返回 null(调用方保留原始技能)。
|
||||
/// </summary>
|
||||
public FormSkillSO GetOverriddenSkill(FormSO form, string slotName)
|
||||
{
|
||||
foreach (var o in _slotOverrides)
|
||||
{
|
||||
bool formMatch = o.targetForm == null || o.targetForm == form;
|
||||
if (formMatch && o.targetSlot == slotName)
|
||||
return o.replacementSkill;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Skills/SkillModifierRegistry.cs.meta
Normal file
11
Assets/Scripts/Skills/SkillModifierRegistry.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fab214bcdee30844bb3399c3b487cffa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
14
Assets/Scripts/Skills/SkillSlotNames.cs
Normal file
14
Assets/Scripts/Skills/SkillSlotNames.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace BaseGames.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// 技能插槽名称常量(架构 09_ProgressionModule §10)。
|
||||
/// 供 <see cref="SkillSlotOverride.targetSlot"/> 和 InputReaderSO.BindActions() 共同引用,
|
||||
/// 避免魔法字符串散落于多处。
|
||||
/// </summary>
|
||||
public static class SkillSlotNames
|
||||
{
|
||||
public const string SoulSkill = "SoulSkill";
|
||||
public const string SpiritSkill1 = "SpiritSkill1";
|
||||
public const string SpiritSkill2 = "SpiritSkill2";
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Skills/SkillSlotNames.cs.meta
Normal file
11
Assets/Scripts/Skills/SkillSlotNames.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1ec13dbdf5778ec47b4b44f506506a45
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user