多轮审查和修复

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

@@ -9,7 +9,12 @@
"rootNamespace": "BaseGames.Equipment",
"references": [
"BaseGames.Core.Events",
"BaseGames.Player"
"BaseGames.Player",
"BaseGames.Player.States",
"BaseGames.Combat",
"BaseGames.Feedback",
"BaseGames.Skills",
"BaseGames.Core.Save"
],
"autoReferenced": true,
"overrideReferences": false,

View File

@@ -0,0 +1,25 @@
using UnityEngine;
namespace BaseGames.Equipment
{
/// <summary>
/// 护符目录 SO。
/// 全局唯一资产Assets/Data/Equipment/CharmCatalog.asset
/// 通过 charmId 查找 CharmSO 引用。
/// 由 EquipmentManager 在 AddToCollection / OnLoad 时查询。
/// </summary>
[CreateAssetMenu(menuName = "Equipment/CharmCatalog")]
public class CharmCatalogSO : ScriptableObject
{
[SerializeField] private CharmSO[] _charms;
/// <summary>按 charmId 查找护符,找不到返回 null。</summary>
public CharmSO Find(string charmId)
{
if (_charms == null || string.IsNullOrEmpty(charmId)) return null;
foreach (var charm in _charms)
if (charm != null && charm.charmId == charmId) return charm;
return null;
}
}
}

View File

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

View File

@@ -0,0 +1,42 @@
using System.Collections.Generic;
using UnityEngine;
using BaseGames.Core.Events;
namespace BaseGames.Equipment
{
/// <summary>
/// 护符数据 SO架构 09_ProgressionModule §3
/// 资产路径: Assets/ScriptableObjects/Equipment/Charms/Charm_{Name}.asset
/// </summary>
[CreateAssetMenu(menuName = "Equipment/Charm")]
public class CharmSO : ScriptableObject
{
[Header("Identity")]
public string charmId; // 全局唯一 ID如 "Charm_QuickSlash"
public string displayNameKey; // 本地化 Key
[TextArea(2, 4)]
public string descriptionKey;
[Header("Visual")]
public Sprite icon;
public Color glowColor;
[Header("Slot Cost")]
[Range(1, 4)]
public int notchCost; // 占用笔记数1~4
[Header("Effects")]
[SerializeReference]
public List<ICharmEffect> effects = new(); // 多态序列化
[Header("Lore")]
public bool isUnique;
public string unlockHint;
}
/// <summary>
/// 护符事件频道EVT_CharmEquipped / EVT_CharmUnequipped
/// </summary>
[CreateAssetMenu(menuName = "Events/CharmEvent")]
public class CharmEventChannelSO : BaseEventChannelSO<CharmSO> { }
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6dec97e009cac9d4b81beb1f78478f73
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
using System;
using UnityEngine;
namespace BaseGames.Equipment
{
/// <summary>
/// 攻击速度加成护符效果(架构 09_ProgressionModule §5
/// 修改 PlayerStats.AnimatorSpeedMultiplier动画控制器查询该倒数应用速度。
/// </summary>
[Serializable]
public class AttackSpeedEffect : ICharmEffect
{
[Range(0.1f, 2.0f)]
public float speedMultiplier = 1.2f; // 动画速度倍率1.2 = 加速 20%
public void OnEquip(EquipmentContext ctx)
{
ctx.Stats?.AddAnimatorSpeedBonus(speedMultiplier - 1f);
}
public void OnUnequip(EquipmentContext ctx)
{
ctx.Stats?.RemoveAnimatorSpeedBonus(speedMultiplier - 1f);
}
public string GetEffectDescription() => $"攻击速度 +{(speedMultiplier - 1f) * 100:0}%";
}
}

View File

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

View File

@@ -0,0 +1,69 @@
using System;
using UnityEngine;
using BaseGames.Combat;
using BaseGames.Core.Events;
namespace BaseGames.Equipment
{
/// <summary>
/// 命中触发效果类型。
/// </summary>
public enum OnHitEffectType
{
ApplyPoison,
ApplyFire,
KnockbackBoost
}
/// <summary>
/// 命中触发护符效果(架构 09_ProgressionModule §5
/// 订阅 HitConfirmedEventChannelSO("EVT_HitConfirmed") 并按概率对命中目标施加状态效果。
/// KnockbackBoost 类型通过 DamageInfo.KnockbackForce 标记,由 HurtBox 流水线读取。
/// </summary>
[Serializable]
public class OnHitEffect : ICharmEffect
{
public OnHitEffectType effectType;
[Range(0f, 1f)]
public float chance; // 触发概率0~1
private HitConfirmedEventChannelSO _onHitChannel;
private EventSubscription _sub;
public void OnEquip(EquipmentContext ctx)
{
_onHitChannel = ctx.Events?.Get<HitConfirmedEventChannelSO>("EVT_HitConfirmed");
_sub = _onHitChannel?.Subscribe(HandleHit);
}
public void OnUnequip(EquipmentContext ctx)
{
_sub?.Dispose();
_sub = null;
_onHitChannel = null;
}
private void HandleHit(HitInfo info)
{
if (UnityEngine.Random.value > chance) return;
switch (effectType)
{
case OnHitEffectType.ApplyPoison:
info.HitTransform?.GetComponentInParent<IStatusEffectable>()
?.ApplyStatusEffect(DamageType.Poison);
break;
case OnHitEffectType.ApplyFire:
info.HitTransform?.GetComponentInParent<IStatusEffectable>()
?.ApplyStatusEffect(DamageType.Fire);
break;
case OnHitEffectType.KnockbackBoost:
// KnockbackBoost 在 HitBox 构建 DamageInfo 前生效,
// 此处为命中后的反馈(可在此播放特效;实际击退已在 HurtBox 流水线处理)。
break;
}
}
public string GetEffectDescription() => $"命中时 {chance * 100:0}% 概率附加 {effectType}";
}
}

View File

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

View File

@@ -0,0 +1,27 @@
using System;
using BaseGames.Skills;
namespace BaseGames.Equipment
{
/// <summary>
/// 技能数值修改护符效果(架构 09_ProgressionModule §5
/// 通过 SkillModifierRegistry 对指定技能的数值加成。
/// </summary>
[Serializable]
public class SkillNumericModifierEffect : ICharmEffect
{
public string TargetSkillId;
public SkillStat Stat;
public float Delta;
public bool IsPercent;
public void OnEquip(EquipmentContext ctx)
=> ctx.SkillMods?.Register(TargetSkillId, Stat, Delta, IsPercent);
public void OnUnequip(EquipmentContext ctx)
=> ctx.SkillMods?.Unregister(TargetSkillId, Stat, Delta, IsPercent);
public string GetEffectDescription()
=> $"{TargetSkillId}.{Stat} {(Delta >= 0 ? "+" : "")}{Delta}{(IsPercent ? "%" : "")}";
}
}

View File

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

View File

@@ -0,0 +1,30 @@
using System;
using BaseGames.Skills;
namespace BaseGames.Equipment
{
/// <summary>
/// 技能插槽替换护符效果(架构 09_ProgressionModule §5
/// 将指定形态的某技能槽替换为另一技能。
/// </summary>
[Serializable]
public class SkillSlotOverrideEffect : ICharmEffect
{
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.displayNameKey : "null";
return $"{formStr}的 {overrideData.targetSlot} 替换为 [{skillName}]";
}
}
}

View File

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

View File

@@ -0,0 +1,36 @@
using System;
namespace BaseGames.Equipment
{
/// <summary>
/// 法术资源类型。
/// </summary>
public enum SpellType
{
SoulAttack,
HealingWave
}
/// <summary>
/// 灵魂法术强化护符效果(架构 09_ProgressionModule §5
/// 装备时减少灵力消耗卸下时还原。SpellManager 消耗时查询 PlayerStats.SoulCostReduction。
/// </summary>
[Serializable]
public class SoulSpellEffect : ICharmEffect
{
public SpellType spellType;
public int soulCostReduction; // 减少消耗的灵力点数
public void OnEquip(EquipmentContext ctx)
{
ctx.Stats?.AddSoulCostReduction(soulCostReduction);
}
public void OnUnequip(EquipmentContext ctx)
{
ctx.Stats?.RemoveSoulCostReduction(soulCostReduction);
}
public string GetEffectDescription() => $"{spellType} 消耗减少 {soulCostReduction} 灵力";
}
}

View File

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

View File

@@ -0,0 +1,28 @@
using System;
namespace BaseGames.Equipment
{
/// <summary>
/// 属性加成护符效果(架构 09_ProgressionModule §5
/// 装备时通过 PlayerStats.AddModifier 叠加固定/百分比属性加成。
/// </summary>
[Serializable]
public class StatModifierEffect : ICharmEffect
{
public StatType statType;
public float flatBonus; // 固定加成(如 +1 HP
public float percentBonus; // 百分比加成(如 +0.2 = +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} +{percentBonus * 100:0}%";
}
}

View File

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

View File

@@ -0,0 +1,29 @@
using System;
using BaseGames.Player;
namespace BaseGames.Equipment
{
/// <summary>
/// 武器替换护符效果(架构 09_ProgressionModule §5
/// 通过 WeaponManager 将指定形态的武器替换为另一武器。
/// </summary>
[Serializable]
public class WeaponOverrideEffect : ICharmEffect
{
public string targetFormId; // 目标形态 ID留空 = 所有形态)
public WeaponSO replacementWeapon; // 替换武器 SO
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}]";
}
}
}

View File

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

View File

@@ -0,0 +1,20 @@
using UnityEngine;
namespace BaseGames.Equipment
{
/// <summary>
/// 装备系统全局配置 SO架构 09_ProgressionModule §6
/// 资产路径: Assets/ScriptableObjects/Equipment/EquipmentConfig.asset
/// </summary>
[CreateAssetMenu(menuName = "Equipment/EquipmentConfig")]
public class EquipmentConfigSO : ScriptableObject
{
[Header("Notch 配置")]
[Tooltip("初始可用笔记数")]
public int initialNotchCount = 3;
[Header("收集上限")]
[Tooltip("护符收藏槽上限(-1 = 无限制)")]
public int maxCollectionSize = -1;
}
}

View File

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

View File

@@ -0,0 +1,143 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using BaseGames.Core;
using BaseGames.Player;
using BaseGames.Core.Events;
using BaseGames.Core.Save;
using BaseGames.Skills;
using BaseGames.Feedback;
namespace BaseGames.Equipment
{
/// <summary>
/// 装备管理器(架构 09_ProgressionModule §6
/// 挂在 Player 上,管理护符的装备/卸下及 Notch 容量。
/// 实现 ISaveable 以持久化装备状态。
/// </summary>
public class EquipmentManager : MonoBehaviour, ISaveable
{
[Header("配置")]
[SerializeField] private EquipmentConfigSO _config;
[SerializeField] private CharmCatalogSO _charmCatalog;
[Header("Event Channels")]
[SerializeField] private CharmEventChannelSO _onCharmEquipped;
[SerializeField] private CharmEventChannelSO _onCharmUnequipped;
[SerializeField] private VoidEventChannelSO _onEquipmentChanged;
private readonly List<CharmSO> _equipped = new(4);
private readonly List<CharmSO> _collected = new(32);
private int _currentNotchCapacity;
private int _usedNotches;
private EquipmentContext _ctx;
// ── 生命周期 ──────────────────────────────────────────────────────────
private void Awake()
{
_ctx = new EquipmentContext
{
Stats = GetComponent<PlayerStats>(),
Feedback = GetComponent<PlayerFeedback>(),
Events = ServiceLocator.GetOrDefault<IEventChannelRegistry>(),
SkillMods = GetComponent<SkillModifierRegistry>(),
WeaponMgr = GetComponent<WeaponManager>(),
};
Debug.Assert(_config != null, "[EquipmentManager] _config 未赋值,请在 Inspector 中指定 EquipmentConfigSO。", this);
Debug.Assert(_charmCatalog != null, "[EquipmentManager] _charmCatalog 未赋值,请在 Inspector 中指定 CharmCatalogSO。", this);
_currentNotchCapacity = _config.initialNotchCount;
Debug.Assert(_ctx.Stats != null, "[EquipmentManager] 缺少 PlayerStats护符效果无法修改属性。", this);
Debug.Assert(_ctx.Feedback != null, "[EquipmentManager] 缺少 PlayerFeedback护符效果无法触发反馈。", this);
Debug.Assert(_ctx.SkillMods != null, "[EquipmentManager] 缺少 SkillModifierRegistry。", this);
}
// ── 查询属性 ─────────────────────────────────────────────────────────
public int UsedNotches => _usedNotches; // 缓存值,避免每次调用 LINQ Sum
public int TotalNotches => _currentNotchCapacity;
public IReadOnlyList<CharmSO> Equipped => _equipped;
public IReadOnlyList<CharmSO> Collected => _collected;
// ── 装备操作 ─────────────────────────────────────────────────────────
/// <summary>
/// 装备护符。返回 null 表示成功;返回错误字符串表示失败原因。
/// </summary>
public string TryEquipCharm(CharmSO charm)
{
if (charm == null) return "护符不存在";
if (_equipped.Contains(charm)) return "已经装备";
if (!_collected.Contains(charm)) return "尚未收集此护符";
int remaining = _currentNotchCapacity - UsedNotches;
if (charm.notchCost > remaining)
return $"笔记不足(需要 {charm.notchCost},剩余 {remaining}";
_equipped.Add(charm);
_usedNotches += charm.notchCost;
foreach (var fx in charm.effects) fx?.OnEquip(_ctx);
_onCharmEquipped?.Raise(charm);
_onEquipmentChanged?.Raise();
return null;
}
public void UnequipCharm(CharmSO charm)
{
if (charm == null || !_equipped.Remove(charm)) return;
_usedNotches -= charm.notchCost;
foreach (var fx in charm.effects) fx?.OnUnequip(_ctx);
_onCharmUnequipped?.Raise(charm);
_onEquipmentChanged?.Raise();
}
/// <summary>将护符加入收藏(拾取时调用)。</summary>
public void AddToCollection(string charmId)
{
var charm = _charmCatalog.Find(charmId);
if (charm == null) { Debug.LogWarning($"[EquipmentManager] 找不到护符: {charmId}"); return; }
if (!_collected.Contains(charm))
_collected.Add(charm);
}
public void IncreaseNotches(int amount)
{
_currentNotchCapacity += amount;
_onEquipmentChanged?.Raise();
}
// ── ISaveable ────────────────────────────────────────────────────────
public void OnSave(SaveData data)
{
data.Equipment.EquippedCharmIds.Clear();
data.Equipment.EquippedCharmIds.AddRange(_equipped.Select(c => c.charmId));
data.Equipment.OwnedCharmIds.Clear();
data.Equipment.OwnedCharmIds.AddRange(_collected.Select(c => c.charmId));
data.Equipment.NotchesUsed = UsedNotches;
}
public void OnLoad(SaveData data)
{
// 卸下所有当前护符(不触发事件,逆序遍历避免 ToList() GC 分配)
for (int i = _equipped.Count - 1; i >= 0; i--)
foreach (var fx in _equipped[i].effects) fx?.OnUnequip(_ctx);
_equipped.Clear();
_usedNotches = 0;
// 从 CharmCatalog 按 ID 恢复收藏并重新装备
_collected.Clear();
foreach (var id in data.Equipment.OwnedCharmIds)
{
var charm = _charmCatalog.Find(id);
if (charm != null && !_collected.Contains(charm))
_collected.Add(charm);
}
foreach (var id in data.Equipment.EquippedCharmIds)
{
var charm = _charmCatalog.Find(id);
if (charm != null) TryEquipCharm(charm);
}
_onEquipmentChanged?.Raise();
}
}
}

View File

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

View File

@@ -0,0 +1,32 @@
using System;
using BaseGames.Player;
using BaseGames.Combat;
using BaseGames.Core.Events;
using BaseGames.Skills;
using BaseGames.Feedback;
namespace BaseGames.Equipment
{
/// <summary>
/// 护符效果接口(架构 09_ProgressionModule §4
/// 所有护符效果均实现此接口,支持 [SerializeReference] 多态序列化。
/// </summary>
public interface ICharmEffect
{
void OnEquip(EquipmentContext ctx);
void OnUnequip(EquipmentContext ctx);
string GetEffectDescription();
}
/// <summary>
/// 护符效果上下文:避免接口直接依赖具体 Manager 类(架构 09_ProgressionModule §4
/// </summary>
public struct EquipmentContext
{
public PlayerStats Stats;
public PlayerFeedback Feedback;
public IEventChannelRegistry Events; // SO 事件频道注册表
public SkillModifierRegistry SkillMods; // 技能修改器注册表
public WeaponManager WeaponMgr; // 武器切换管理器
}
}

View File

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

View File

@@ -0,0 +1,25 @@
using UnityEngine;
namespace BaseGames.Equipment
{
/// <summary>
/// 工具目录 SO。
/// 全局唯一资产Assets/Data/Equipment/ToolCatalog.asset
/// 通过 toolId 查找 ToolSO 引用。
/// 由 ToolSlotManager 在 OnLoad 时查询以恢复槽位工具引用。
/// </summary>
[CreateAssetMenu(menuName = "Equipment/ToolCatalog")]
public class ToolCatalogSO : ScriptableObject
{
[SerializeField] private ToolSO[] _tools;
/// <summary>按 toolId 查找工具,找不到返回 null。</summary>
public ToolSO Find(string toolId)
{
if (_tools == null || string.IsNullOrEmpty(toolId)) return null;
foreach (var tool in _tools)
if (tool != null && tool.toolId == toolId) return tool;
return null;
}
}
}

View File

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

View File

@@ -0,0 +1,51 @@
using System;
using UnityEngine;
using BaseGames.Player.States;
namespace BaseGames.Equipment
{
/// <summary>
/// 工具使用效果接口(架构 09_ProgressionModule §7
/// </summary>
public interface IToolEffect
{
void Use(PlayerController player);
}
/// <summary>
/// 带冷却时间的工具接口(可选实现)。
/// </summary>
public interface IToolCooldown
{
float CooldownDuration { get; }
}
/// <summary>
/// 典型实现:治疗药水效果。
/// </summary>
[Serializable]
public class HealToolEffect : IToolEffect
{
public int HealAmount = 4;
public void Use(PlayerController player) => player.Stats.HealHP(HealAmount);
}
/// <summary>
/// 主动工具数据 SO架构 09_ProgressionModule §7
/// 资产路径: Assets/ScriptableObjects/Equipment/Tools/Tool_{Name}.asset
/// </summary>
[CreateAssetMenu(menuName = "Equipment/Tool")]
public class ToolSO : ScriptableObject
{
public string toolId;
public string displayNameKey;
public Sprite icon;
[Tooltip("-1 = 无限使用次数")]
public int maxUses = 1;
[SerializeReference]
public IToolEffect effect; // 工具使用效果(多态)
}
}

View File

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

View File

@@ -0,0 +1,91 @@
using UnityEngine;
using BaseGames.Core.Events;
using BaseGames.Core.Save;
using BaseGames.Player.States;
namespace BaseGames.Equipment
{
/// <summary>
/// 工具槽管理器(架构 09_ProgressionModule §7.5)。
/// 管理玩家的 2 个工具槽(装备、使用、冷却)。
/// 实现 ISaveable 以持久化槽位状态。
/// </summary>
public class ToolSlotManager : MonoBehaviour, ISaveable
{
private const int SlotCount = 2;
[SerializeField] private ToolCatalogSO _toolCatalog;
[SerializeField] private ToolSO[] _slots = new ToolSO[SlotCount];
[SerializeField] private int[] _remainingUses = new int[SlotCount]; // -1 = 无限
[SerializeField] private ToolUsedEventChannelSO _onToolUsed;
private readonly float[] _cooldowns = new float[SlotCount];
private void Awake()
{
Debug.Assert(_toolCatalog != null, "[ToolSlotManager] _toolCatalog 未赋值,请在 Inspector 中指定 ToolCatalogSO。", this);
}
// ── 装备 ─────────────────────────────────────────────────────────────
public void EquipTool(int slotIndex, ToolSO tool)
{
if (slotIndex < 0 || slotIndex >= SlotCount) return;
_slots[slotIndex] = tool;
_remainingUses[slotIndex] = tool != null ? tool.maxUses : 0;
_cooldowns[slotIndex] = 0f;
}
// ── 使用 ─────────────────────────────────────────────────────────────
public bool TryUseTool(int slotIndex, PlayerController player)
{
if (slotIndex < 0 || slotIndex >= SlotCount) return false;
var tool = _slots[slotIndex];
if (tool == null) return false;
if (_cooldowns[slotIndex] > 0f) return false;
if (_remainingUses[slotIndex] == 0) return false; // 已耗尽(-1 = 无限不触发)
tool.effect?.Use(player);
if (_remainingUses[slotIndex] > 0) _remainingUses[slotIndex]--;
_cooldowns[slotIndex] = tool is IToolCooldown tc ? tc.CooldownDuration : 0f;
_onToolUsed?.Raise(new ToolUsedPayload
{
SlotIndex = slotIndex,
ToolId = tool.toolId
});
return true;
}
private void Update()
{
for (int i = 0; i < SlotCount; i++)
if (_cooldowns[i] > 0f) _cooldowns[i] -= Time.deltaTime;
}
// ── 查询 ─────────────────────────────────────────────────────────────
public ToolSO GetTool(int slotIndex) => _slots[slotIndex];
public int GetRemainingUses(int slotIndex) => _remainingUses[slotIndex];
public float GetCooldownRatio(int slotIndex)
{
if (_slots[slotIndex] is IToolCooldown tc && tc.CooldownDuration > 0f)
return _cooldowns[slotIndex] / tc.CooldownDuration;
return 0f;
}
// ── ISaveable ────────────────────────────────────────────────────────
public void OnSave(SaveData data)
{
data.Tools.ToolSlot0 = _slots[0]?.toolId;
data.Tools.ToolSlot1 = _slots[1]?.toolId;
}
public void OnLoad(SaveData data)
{
_cooldowns[0] = _cooldowns[1] = 0f;
EquipTool(0, _toolCatalog.Find(data.Tools.ToolSlot0));
EquipTool(1, _toolCatalog.Find(data.Tools.ToolSlot1));
}
}
}

View File

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