Files
zeling_v2/Docs/Design/17_EquipmentSystem.md
2026-05-08 11:04:00 +08:00

682 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 17 · 装备系统(魅力/工具槽)
> **命名空间** `BaseGames.Equipment`
> **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md)
> **依赖** `BaseGames.Core.Events` · `BaseGames.Player`PlayerStats· `BaseGames.UI`
---
## 目录
1. [系统总览](#1-系统总览)
2. [CharmSO — 魅力数据](#2-charmso--魅力数据)
3. [ICharmEffect — 效果接口](#3-icharmeffect--效果接口)
4. [内置魅力效果实现](#4-内置魅力效果实现)
- [4.1 StatModifierEffect](#41-statmodifiereffect--属性加成)
- [4.2 AttackSpeedEffect](#42-attackspeedeffect--攻击速度加成)
- [4.3 OnHitEffect](#43-onhiteffect--命中触发效果)
- [4.4 SoulSpellEffect](#44-soulspelleffect--灵魂法术强化)
- [4.5 SkillNumericModifierEffect](#45-skillnumericmodifiereffect--技能数值修改)
- [4.6 SkillSlotOverrideEffect](#46-skillslotoverrideeffect--技能插槽替换)
- [4.7 WeaponOverrideEffect](#47-weaponoverrideeffect--武器替换)
5. [EquipmentManager — 槽位管理](#5-equipmentmanager--槽位管理)
6. [槽位系统与笔记Notch](#6-槽位系统与笔记notch)
7. [ToolSO — 主动工具](#7-toolso--主动工具)
8. [装备 UI](#8-装备-ui)
9. [SaveData 集成](#9-savedata-集成)
10. [事件频道](#10-事件频道)
11. [编辑器友好设计](#11-编辑器友好设计)
---
## 1. 系统总览
装备系统Charm/Tool 系统)是银河恶魔城游戏元进度的核心:玩家在世界中收集魅力,在存档点装备,通过组合不同魅力改变战斗风格。对标游戏《丝之歌》中的 Threads 系统。
```
装备系统职责:
├─ CharmSO → 魅力数据 SO效果、槽位占用、图标
├─ ICharmEffect → 效果注入接口(无脚本级耦合,通过接口多态)
├─ EquipmentManager → 装备/卸下逻辑、槽位管理、装备状态查询
├─ ToolSO → 主动工具(有限使用次数的消耗品/无限技能)
└─ EquipmentUI → 装备槽 HUD、装备面板 UI、收集列表
```
**零耦合原则**`EquipmentManager` 通过 SO 事件频道发布装备变更通知,`PlayerStats` 等订阅方自行响应;不持有 `PlayerController` 直接引用。
---
## 2. CharmSO — 魅力数据
```csharp
[CreateAssetMenu(menuName = "Equipment/Charm")]
public class CharmSO : ScriptableObject
{
[Header("基础信息")]
public string charmId; // 全局唯一 ID如 "Charm_QuickSlash"
public string displayName; // 显示名称(本地化 key 或直接填写)
[TextArea(3, 6)]
public string description; // 描述文本
[Header("外观")]
public Sprite icon; // 魅力图标
public Color glowColor; // 魅力光晕颜色
[Header("槽位占用")]
[Range(1, 4)]
public int notchCost; // 占用的 Notch 数量1~4
[Header("效果")]
[SerializeReference] // 支持多态序列化
public List<ICharmEffect> effects; // 魅力效果列表(可叠加多个)
[Header("获得方式")]
public bool isUnique; // 唯一物品,只能携带一个
public string unlockHint; // 提示文本(从何处获得)
}
```
**资产存放路径**`Assets/ScriptableObjects/Equipment/Charms/`
**命名规范**`Charm_{Name}.asset`
---
## 3. ICharmEffect — 效果接口
```csharp
namespace BaseGames.Equipment
{
/// <summary>
/// 魅力效果接口。装备时 OnEquip卸下时 OnUnequip。
/// 所有效果通过修改 PlayerStats 或注册事件监听来生效,不直接引用 PlayerController。
/// </summary>
[Serializable]
public interface ICharmEffect
{
void OnEquip(EquipmentContext ctx);
void OnUnequip(EquipmentContext ctx);
string GetEffectDescription(); // Inspector 与 UI Tooltip 显示
}
/// <summary>
/// 效果上下文(注入依赖,避免全局引用)
/// </summary>
public struct EquipmentContext
{
public PlayerStats Stats;
public PlayerFeedback Feedback;
public EventChannelRegistry Events; // SO 事件频道注册表
public SkillModifierRegistry SkillMods; // 技能修改器注册表(见 21_SpellSystem §5
public WeaponManager WeaponMgr; // 武器切换管理器(见 53_WeaponSystem §3
}
}
```
---
## 4. 内置魅力效果实现
### 4.1 StatModifierEffect — 属性加成
```csharp
[Serializable]
public class StatModifierEffect : ICharmEffect
{
public StatType statType; // MaxHP / AttackDamage / MoveSpeed / JumpHeight / SoulGain
public float flatBonus; // 固定加成(如 +1 HP
public float percentBonus;// 百分比加成(如 +20%
public void OnEquip(EquipmentContext ctx)
=> ctx.Stats.AddModifier(statType, flatBonus, percentBonus);
public void OnUnequip(EquipmentContext ctx)
=> ctx.Stats.RemoveModifier(statType, flatBonus, percentBonus);
public string GetEffectDescription()
=> $"{statType}: {(flatBonus > 0 ? $"+{flatBonus}" : "")}" +
$"{(percentBonus > 0 ? $" +{percentBonus*100:0}%" : "")}";
}
```
**可加成属性列表**
| StatType | 说明 | 示例魅力 |
|---------|------|---------|
| `MaxHP` | 最大生命值 | 强心魅(+2 HP|
| `AttackDamage` | 攻击伤害 | 利刃魅(+15% 攻击)|
| `MoveSpeed` | 移动速度 | 疾步魅(+0.5 m/s|
| `JumpHeight` | 跳跃高度 | 轻跃魅(+20%|
| `SoulGain` | 每次命中获得 Soul | 汲魂魅(+5 Soul/hit|
| `Defense` | 减少受到的伤害 | 护甲魅(+1 防御)|
### 4.2 AttackSpeedEffect — 攻击速度加成
```csharp
[Serializable]
public class AttackSpeedEffect : ICharmEffect
{
[Range(0.1f, 1.0f)]
public float speedMultiplier; // 动画速度倍率(如 1.3 = 加速 30%
public void OnEquip(EquipmentContext ctx)
=> ctx.Stats.AnimatorSpeedMultiplier += (speedMultiplier - 1f);
public void OnUnequip(EquipmentContext ctx)
=> ctx.Stats.AnimatorSpeedMultiplier -= (speedMultiplier - 1f);
public string GetEffectDescription()
=> $"攻击速度 +{(speedMultiplier - 1) * 100:0}%";
}
```
### 4.3 OnHitEffect — 命中触发效果
```csharp
[Serializable]
public class OnHitEffect : ICharmEffect
{
public OnHitEffectType effectType; // ApplyPoison / ApplyFire / KnockbackBoost
[Range(0f, 1f)]
public float chance; // 触发概率0~1
DamageInfoEventChannelSO _onHitChannel;
public void OnEquip(EquipmentContext ctx)
{
_onHitChannel = ctx.Events.Get<DamageInfoEventChannelSO>("OnHitConfirmed");
_onHitChannel.OnEventRaised += HandleHit;
}
public void OnUnequip(EquipmentContext ctx)
=> _onHitChannel.OnEventRaised -= HandleHit;
void HandleHit(DamageInfo info)
{
if (Random.value > chance) return;
// 触发对应效果(由 StatusEffectManager 处理,见 04_CombatSystem §12
}
public string GetEffectDescription()
=> $"命中时 {chance*100:0}% 概率附加 {effectType}";
}
```
### 4.4 SoulSpellEffect — 灵魂法术强化
```csharp
[Serializable]
public class SoulSpellEffect : ICharmEffect
{
public SpellType spellType; // SoulAttack / HealingWave具体法术
public int soulCostReduction; // 减少消耗 Soul 点数
public void OnEquip(EquipmentContext ctx)
=> ctx.Stats.RegisterSpellModifier(spellType, soulCostReduction, 0f);
public void OnUnequip(EquipmentContext ctx)
=> ctx.Stats.UnregisterSpellModifier(spellType, soulCostReduction, 0f);
public string GetEffectDescription()
=> $"{spellType} 消耗减少 {soulCostReduction} Soul";
}
```
### 4.5 SkillNumericModifierEffect — 技能数值修改
修改现有技能的伤害、范围、消耗、冷却和特效(不替换技能本身):
```csharp
[Serializable]
public class SkillNumericModifierEffect : ICharmEffect
{
[Tooltip("数值修改配置,可指定目标技能 ID / 资源类型 / 各倍率 / 特效覆盖")]
public SkillNumericModifier modifier;
public void OnEquip(EquipmentContext ctx) => ctx.SkillMods.AddNumericMod(modifier);
public void OnUnequip(EquipmentContext ctx) => ctx.SkillMods.RemoveNumericMod(modifier);
public string GetEffectDescription()
{
var parts = new System.Collections.Generic.List<string>();
if (!Mathf.Approximately(modifier.damageMult, 1f)) parts.Add($"伤害×{modifier.damageMult:0.##}");
if (!Mathf.Approximately(modifier.rangeMult, 1f)) parts.Add($"范围×{modifier.rangeMult:0.##}");
if (!Mathf.Approximately(modifier.costMult, 1f)) parts.Add($"消耗×{modifier.costMult:0.##}");
if (!Mathf.Approximately(modifier.cooldownMult, 1f)) parts.Add($"冷却×{modifier.cooldownMult:0.##}");
if (modifier.vfxOverride != null) parts.Add("特效替换");
if (modifier.animOverride != null) parts.Add("动画替换");
string target = string.IsNullOrEmpty(modifier.targetSkillId)
? "全部技能" : modifier.targetSkillId;
return $"{target}{string.Join(" / ", parts)}";
}
}
```
**配置示例**
```
// 范例护符:【裂空掌强化符】(+50% 伤害 / +30% 范围,仅对裂空掌)
modifier.targetSkillId = "sky_soul_skill"
modifier.damageMult = 1.5
modifier.rangeMult = 1.3
modifier.costMult = 1.0
modifier.cooldownMult = 1.0
// 范例护符:【魄元节笔】(所有魄技能消耗 -25%
modifier.targetSkillId = "" // 留空 = 匹配所有
modifier.resourceFilter = SpiritOnly
modifier.costMult = 0.75
// 范例护符:【异形灵踪】(替换灵踪弹特效为火焰弹碗)
modifier.targetSkillId = "sky_spirit_skill2"
modifier.vfxOverride = FeedbackPreset_FireballVFX // 刹射物设计不变,只改特效
```
### 4.6 SkillSlotOverrideEffect — 技能插槽替换
将某形态某槽位的技能 SO **整体替换**为另一个 FormSkillSO
```csharp
[Serializable]
public class SkillSlotOverrideEffect : ICharmEffect
{
[Tooltip("插槽替换配置targetFormnull=全部形态)+ targetSlot + replacementSkill + priority")]
public SkillSlotOverride overrideData;
public void OnEquip(EquipmentContext ctx) => ctx.SkillMods.AddSlotOverride(overrideData);
public void OnUnequip(EquipmentContext ctx) => ctx.SkillMods.RemoveSlotOverride(overrideData);
public string GetEffectDescription()
{
string formStr = overrideData.targetForm != null
? overrideData.targetForm.name : "所有形态";
string skillName = overrideData.replacementSkill != null
? overrideData.replacementSkill.displayName : "null";
return $"{formStr}的 {overrideData.targetSlot} 替换为 [{skillName}]";
}
}
```
**配置示例**
```
// 范例护符:【混元引导符】(天魂形态的魂技能替换为地行术)
overrideData.targetForm = FormSO_SkyForm
overrideData.targetSlot = SoulSkill
overrideData.replacementSkill = FormSkillSO_DiXingShu
overrideData.priority = 0
// 范例护符【夜行者印记】任意形态的魄技能2 替换为残阴术)
overrideData.targetForm = null // 全部形态均生效
overrideData.targetSlot = SpiritSkill2
overrideData.replacementSkill = FormSkillSO_CanYinShu
overrideData.priority = 1 // 高于其他护符,确保生效
```
**冲突解决规则**
| 场景 | 结果 |
|------|---------|
| 两个护符替换同一槽位,`priority` 不同 | 高 `priority` 者生效 |
| 两个护符替换同一槽位,`priority` 相同 | 后装备的护符生效 |
| 一个替换全部形态,一个替换特定形态 | 特定形态的护符在该形态中优先targetForm != null 得到额外 +1 优先加成,见 SkillModifierRegistry |
---
### 4.7 WeaponOverrideEffect — 武器替换
将某形态(或所有形态)的默认武器整体替换为另一个 `WeaponSO`(见 [53_WeaponSystem §6](./53_WeaponSystem.md#6-护符武器覆盖weaponoverrideeffect)
```csharp
[Serializable]
public class WeaponOverrideEffect : ICharmEffect
{
[Tooltip("目标形态 ID留空 = 所有形态)")]
public string targetFormId;
[Tooltip("替换武器 SO")]
public WeaponSO replacementWeapon;
public void OnEquip(EquipmentContext ctx)
=> ctx.WeaponMgr.SetOverride(targetFormId, replacementWeapon);
public void OnUnequip(EquipmentContext ctx)
=> ctx.WeaponMgr.ClearOverride(targetFormId);
public string GetEffectDescription()
{
string formStr = string.IsNullOrEmpty(targetFormId) ? "所有形态" : targetFormId;
string wName = replacementWeapon != null ? replacementWeapon.displayName : "null";
return $"{formStr}的武器替换为 [{wName}]";
}
}
```
**配置示例**
```
// 护符【命魂刃灵符】命魂形态改用高速短刃4 Notch
targetFormId = "LifeForm"
replacementWeapon = Weapon_ShadowDagger
// 护符【混沌铸兵符】所有形态均使用地魂锤3 Notch
targetFormId = "" // 空 = 全部形态
replacementWeapon = Weapon_EarthHammer
```
**冲突规则**:若多个护符同时覆盖同一形态武器,**后装备的护符生效**`Dictionary` 键覆盖语义)。
---
## 5. EquipmentManager — 槽位管理
`EquipmentManager` 常驻 Player GameObject管理装备状态。
```csharp
namespace BaseGames.Equipment
{
public class EquipmentManager : MonoBehaviour
{
[Header("配置")]
[SerializeField] EquipmentConfigSO _config; // 初始 Notch 数量等
[Header("事件频道")]
[SerializeField] CharmEventChannelSO _onCharmEquipped;
[SerializeField] CharmEventChannelSO _onCharmUnequipped;
[SerializeField] VoidEventChannelSO _onEquipmentChanged; // 总变更通知
// 运行时状态
readonly List<CharmSO> _collectedCharms = new(); // 已收集魅力库
readonly List<CharmSO> _equippedCharms = new(); // 当前装备的魅力
int _currentNotchCapacity; // 当前最大 Notch 数(会随升级增加)
int _usedNotches => _equippedCharms.Sum(c => c.notchCost);
EquipmentContext _ctx;
void Awake()
{
var stats = GetComponent<PlayerStats>();
var feedback = GetComponent<PlayerFeedback>();
_ctx = new EquipmentContext {
Stats = stats,
Feedback = feedback,
Events = EventChannelRegistry.Instance,
SkillMods = GetComponent<SkillModifierRegistry>(), // 同一 Player GO
WeaponMgr = GetComponent<WeaponManager>(), // 同一 Player GO
};
}
// ─────────────── 公开 API ───────────────
/// <summary>装备魅力。返回失败原因null = 成功)</summary>
public string TryEquipCharm(CharmSO charm)
{
if (_equippedCharms.Contains(charm)) return "已经装备";
if (!_collectedCharms.Contains(charm)) return "尚未收集此魅力";
int newUsed = _usedNotches + charm.notchCost;
if (newUsed > _currentNotchCapacity)
return $"笔记不足(需要 {charm.notchCost},剩余 {_currentNotchCapacity - _usedNotches}";
_equippedCharms.Add(charm);
foreach (var effect in charm.effects)
effect.OnEquip(_ctx);
_onCharmEquipped.Raise(charm);
_onEquipmentChanged.Raise();
return null;
}
public void UnequipCharm(CharmSO charm)
{
if (!_equippedCharms.Remove(charm)) return;
foreach (var effect in charm.effects)
effect.OnUnequip(_ctx);
_onCharmUnequipped.Raise(charm);
_onEquipmentChanged.Raise();
}
public bool IsEquipped(CharmSO charm) => _equippedCharms.Contains(charm);
public bool IsCollected(CharmSO charm) => _collectedCharms.Contains(charm);
public int UsedNotches => _usedNotches;
public int NotchCapacity => _currentNotchCapacity;
public IReadOnlyList<CharmSO> EquippedCharms => _equippedCharms;
public IReadOnlyList<CharmSO> CollectedCharms => _collectedCharms;
// ─────────────── 存档集成 ───────────────
public void LoadFromSaveData(SaveData data)
{
_collectedCharms.Clear();
_equippedCharms.Clear();
// 卸下所有已装备效果(防止重复加载)
foreach (var c in _equippedCharms)
foreach (var e in c.effects)
e.OnUnequip(_ctx);
_currentNotchCapacity = data.equipment.notchCapacity;
foreach (var id in data.equipment.collectedCharms)
{
var charm = CharmDatabase.Instance.GetCharm(id);
if (charm != null) _collectedCharms.Add(charm);
}
foreach (var id in data.equipment.equippedCharms)
{
var charm = CharmDatabase.Instance.GetCharm(id);
if (charm != null) TryEquipCharm(charm);
}
}
public void WriteToSaveData(SaveData data)
{
data.equipment.notchCapacity = _currentNotchCapacity;
data.equipment.collectedCharms = _collectedCharms.Select(c => c.charmId).ToList();
data.equipment.equippedCharms = _equippedCharms.Select(c => c.charmId).ToList();
}
// ─────────────── Notch 升级 ───────────────
/// <summary>由 AbilityUnlock / 剧情事件调用,增加 Notch 容量</summary>
public void AddNotchCapacity(int amount)
{
_currentNotchCapacity += amount;
_onEquipmentChanged.Raise();
}
}
}
```
---
## 6. 槽位系统与笔记Notch
《丝之歌》对标系统:**Notch笔记** 而非固定槽位数量。
```
初始 Notch 容量: 3
可通过以下途径升级:
├─ 购买特殊 NPC 服务: +1 Notch每次 Geo 费用递增)
├─ 隐藏收集物 NotchShard4 个合 1: +1 Notch
└─ 最大 Notch 容量: 11可配置
```
魅力的 Notch 消耗参考:
| 消耗 | 魅力类型示例 |
|------|------------|
| 1 Notch | 轻微属性加成(+MoveSpeed、+SoulGain|
| 2 Notch | 中等效果(+攻击、命中触发)|
| 3 Notch | 强力被动(+2 HP、攻速大幅提升|
| 4 Notch | 顶级魅力(改变游戏机制)|
---
## 7. ToolSO — 主动工具
主动工具Tool是有限使用次数的消耗品或无限施放的技能。
```csharp
[CreateAssetMenu(menuName = "Equipment/Tool")]
public class ToolSO : ScriptableObject
{
[Header("基础信息")]
public string toolId;
public string displayName;
[TextArea(2, 4)]
public string description;
public Sprite icon;
[Header("使用参数")]
public ToolType toolType; // Consumable / Skill / Throwable
public int maxCount; // 最大持有数量(-1 = 无限)
public int soulCost; // 使用消耗 Soul0 = 无消耗)
public float cooldown; // 冷却时间(秒)
[Header("效果")]
[SerializeReference]
public IToolEffect effect; // 工具使用效果
}
```
---
## 8. 装备 UI
### 8.1 装备面板(全屏/暂停中打开)
```
Canvas_Pause
└── EquipmentPanel
├── LeftPanel — 魅力收集库
│ ├── CharmGrid (ScrollView3列)
│ │ └── CharmItem图标 + Notch 数量 + 状态:未收集/已收集/已装备)
│ └── SearchBar可选P2
├── RightPanel — 装备槽
│ ├── NotchBar可视化 Notch 容量条,已用/总量)
│ │ ├── NotchCell × N已用=橙色,剩余=灰色)
│ │ └── CapacityText "4 / 8"
│ ├── EquippedSlots已装备魅力列表图标 + 卸下按钮)
│ └── OvercharmWarning装备超过容量时显示 "过充咒Overcharmed" 警告,玩家受到双倍伤害)
└── BottomBar — 选中魅力详情
├── CharmIcon大图
├── CharmName + NotchCost
├── DescriptionText
├── EffectListICharmEffect.GetEffectDescription() 列表)
└── ActionButton装备 / 卸下)
```
### 8.2 装备 HUD游戏中快捷栏
```
Canvas_HUD
└── ToolQuickbar (底部中央)
├── ToolSlot_1 (QuickSlot 1) — 绑定工具1图标 + 剩余数量
└── ToolSlot_2 (QuickSlot 2) — 绑定工具2图标 + 剩余数量
```
工具快捷栏在装备面板中可拖拽分配;快捷键:手柄 L1/R1键盘 Q/E。
### 8.3 过充咒Overcharmed机制
参考《空洞骑士》Overcharmed玩家强行装备超出 Notch 容量的魅力组合时,进入 Overcharmed 状态:
```csharp
// 在 EquipmentManager 中检测
bool IsOvercharmed => _usedNotches > _currentNotchCapacity;
// PlayerStats 订阅 _onEquipmentChanged
void HandleEquipmentChanged()
{
bool overcharmed = _equipmentManager.IsOvercharmed;
_damageTakenMultiplier = overcharmed ? 2.0f : 1.0f;
_overcharmedIndicator.SetActive(overcharmed);
}
```
---
## 9. SaveData 集成
`SaveData` 新增 `equipment` 字段:
```json
{
"equipment": {
"notchCapacity": 5,
"collectedCharms": ["Charm_QuickSlash", "Charm_LifeBlood", "Charm_LongNail"],
"equippedCharms": ["Charm_QuickSlash", "Charm_LongNail"],
"quickTools": [
{ "toolId": "Tool_HealSeed", "count": 3 },
{ "toolId": null, "count": 0 }
],
"notchShardsCollected": 2
}
}
```
| 字段 | 说明 |
|------|------|
| `notchCapacity` | 当前 Notch 总容量 |
| `collectedCharms` | 已收集魅力 ID 列表 |
| `equippedCharms` | 当前装备魅力 ID 列表 |
| `quickTools` | 两个快捷工具槽toolId + count|
| `notchShardsCollected` | 已收集 Notch 碎片数量 |
---
## 10. 事件频道
| 频道资产 | 类型 | 发布方 | 主要订阅方 |
|---------|------|--------|----------|
| `OnCharmCollected.asset` | `CharmEventChannelSO` | `AbilityUnlock / Collectible` | `EquipmentManager`(添加到库)、`EquipmentUI`(刷新)|
| `OnCharmEquipped.asset` | `CharmEventChannelSO` | `EquipmentManager` | `PlayerStats`(通过效果已注入)、`HUD` |
| `OnCharmUnequipped.asset` | `CharmEventChannelSO` | `EquipmentManager` | `PlayerStats``HUD` |
| `OnEquipmentChanged.asset` | `VoidEventChannelSO` | `EquipmentManager` | `EquipmentUI`(刷新)、`PlayerStats`Overcharmed 检测)|
| `OnNotchCapacityChanged.asset` | `IntEventChannelSO` | `EquipmentManager` | `EquipmentUI`(刷新 Notch 条)|
| `OnToolUsed.asset` | `ToolEventChannelSO` | `PlayerController` | `EquipmentManager`(扣减数量)、`HUD`(更新计数)|
---
## 11. 编辑器友好设计
### CharmSO 自定义 Inspector
```
┌─ Charm_QuickSlash ─────────────────────────────────────────┐
│ [图标预览] Quick Slash │
│ ID: Charm_QuickSlash Notch Cost: ██░░ 2 │
│ 描述: 大幅提升攻击速度 │
│ ───────────────────────────────────── │
│ 效果列表: │
│ [+] AttackSpeedEffect │
│ speedMultiplier: 1.35 → "攻击速度 +35%" │
│ ───────────────────────────────────── │
│ [+ 添加效果] [在游戏中预览] │
└────────────────────────────────────────────────────────────┘
```
- `[SerializeReference]` 使效果列表在 Inspector 中以多态方式折叠展示
- 每个 `ICharmEffect` 实现类打上 `[Serializable]` 标签,自动出现在添加菜单
### EquipmentManager Scene Gizmo
- 在 Scene 视图中,玩家头顶显示已装备魅力的小图标(仅 Edit Mode 或暂停时显示),便于调试
### CharmDatabase EditorWindow
`Tools → Zeling → Charm Database`
```
┌─ Charm Database ───────────────────────────────────────────┐
│ [自动扫描所有 CharmSO] 搜索: [____________] │
│ │
│ ID | Notch | 收集方式 | 效果数 │
│ Charm_QuickSlash | 2 | Forest_Cave_Boss | 1 │
│ Charm_LifeBlood | 2 | Purchase_Shop01 | 1 │
│ ... │
└────────────────────────────────────────────────────────────┘
```