多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View 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": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a6aafac5a7a203441bfb350aac033a04
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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>
/// 反馈预设 SOFeedbackPresetSO封装一组 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;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a96f0270221c8444b8719f0f9b14c635
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fb39b928827b10245aeab5ea41f862a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fab214bcdee30844bb3399c3b487cffa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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";
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1ec13dbdf5778ec47b4b44f506506a45
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: