From 04aec4cc8ef2d1902170d804f956bb2e2b1a9f19 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 20 May 2026 16:37:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=AD=98=E6=A1=A3?= =?UTF-8?q?=E7=AE=A1=E7=90=86=EF=BC=8C=E5=A2=9E=E5=BC=BA=E9=93=BE=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E8=8E=B7=E5=8F=96=E9=80=BB=E8=BE=91=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20ISaveable=20=E6=8E=A5=E5=8F=A3=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scripts/Core/Save/GameSaveManager.cs | 7 ++- .../Scripts/Equipment/ToolSlotManager.cs | 4 ++ .../Scripts/EventChain/EventChainManager.cs | 32 +++++++++++--- Assets/_Game/Scripts/World/FalseWall.cs | 43 +++++++------------ 4 files changed, 51 insertions(+), 35 deletions(-) diff --git a/Assets/_Game/Scripts/Core/Save/GameSaveManager.cs b/Assets/_Game/Scripts/Core/Save/GameSaveManager.cs index a135abd..74fdf17 100644 --- a/Assets/_Game/Scripts/Core/Save/GameSaveManager.cs +++ b/Assets/_Game/Scripts/Core/Save/GameSaveManager.cs @@ -195,7 +195,12 @@ namespace BaseGames.Core.Save public void SetChainCompleted(string chainId) { _current ??= new SaveData(); _current.EventChains.ChainStates[chainId] = "Completed"; } public IEnumerable GetCompletedChains() - => _current?.EventChains.ChainStates.Keys ?? Enumerable.Empty(); + { + if (_current?.EventChains?.ChainStates == null) return System.Linq.Enumerable.Empty(); + return _current.EventChains.ChainStates + .Where(kv => kv.Value == "Completed") + .Select(kv => kv.Key); + } public bool IsFirstClear(string challengeId) { diff --git a/Assets/_Game/Scripts/Equipment/ToolSlotManager.cs b/Assets/_Game/Scripts/Equipment/ToolSlotManager.cs index 31277cb..a55aaf8 100644 --- a/Assets/_Game/Scripts/Equipment/ToolSlotManager.cs +++ b/Assets/_Game/Scripts/Equipment/ToolSlotManager.cs @@ -1,4 +1,5 @@ using UnityEngine; +using BaseGames.Core; using BaseGames.Core.Events; using BaseGames.Core.Save; using BaseGames.Player.States; @@ -26,6 +27,9 @@ namespace BaseGames.Equipment Debug.Assert(_toolCatalog != null, "[ToolSlotManager] _toolCatalog 未赋值,请在 Inspector 中指定 ToolCatalogSO。", this); } + private void OnEnable() => ServiceLocator.GetOrDefault()?.Register(this); + private void OnDisable() => ServiceLocator.GetOrDefault()?.Unregister(this); + // ── 装备 ───────────────────────────────────────────────────────────── public void EquipTool(int slotIndex, ToolSO tool) { diff --git a/Assets/_Game/Scripts/EventChain/EventChainManager.cs b/Assets/_Game/Scripts/EventChain/EventChainManager.cs index 4512d8a..20d7de5 100644 --- a/Assets/_Game/Scripts/EventChain/EventChainManager.cs +++ b/Assets/_Game/Scripts/EventChain/EventChainManager.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using BaseGames.Core; using BaseGames.Core.Events; +using BaseGames.Core.Save; using UnityEngine; namespace BaseGames.EventChain @@ -12,7 +13,7 @@ namespace BaseGames.EventChain /// 订阅游戏各系统的 SO 事件频道,转发为中继 C# 事件供 ChainCondition.Register() 绑定。 /// 每当收到新事件时,检查所有链的触发条件。 /// - public class EventChainManager : MonoBehaviour + public class EventChainManager : MonoBehaviour, ISaveable { [Header("所有事件链")] [SerializeField] private EventChainSO[] _chains; @@ -49,11 +50,8 @@ namespace BaseGames.EventChain private void Awake() { - // 从存档恢复已完成链 - var sm = ServiceLocator.GetOrDefault(); - if (sm != null) - foreach (var id in sm.GetCompletedChains()) - _completedChains.Add(id); + // 不在此处读取存档数据——LoadAsync 尚未完成,_current 为 null。 + // 已完成链的恢复由 ISaveable.OnLoad 在 LoadAsync 完成后处理。 } private void OnEnable() @@ -64,6 +62,8 @@ namespace BaseGames.EventChain _onRoomEntered?.Subscribe(id => { OnRoomEntered?.Invoke(id); EvaluateAll(); }).AddTo(_subs); _onDialogueCompleted?.Subscribe(id => { OnDialogueCompleted?.Invoke(id); EvaluateAll(); }).AddTo(_subs); + ServiceLocator.GetOrDefault()?.Register(this); + // 向每个 Condition 注册中继事件 // 先重置 SO 资产上的运行时状态,防止跨 PlayMode 会话或多场景加载时状态残留 if (_chains == null) return; @@ -79,6 +79,7 @@ namespace BaseGames.EventChain private void OnDisable() { _subs.Clear(); + ServiceLocator.GetOrDefault()?.Unregister(this); if (_chains == null) return; foreach (var chain in _chains) @@ -87,6 +88,25 @@ namespace BaseGames.EventChain cond?.Unregister(this); } + // ── ISaveable ───────────────────────────────────────────────────────── + /// + /// SetChainCompleted 通过 ISaveService 直接写入 SaveData,OnSave 无需操作。 + /// + public void OnSave(SaveData data) { } + + /// + /// LoadAsync 完成后由 GameSaveManager.Register 补发,恢复本会话前已完成的链。 + /// 仅恢复状态为 "Completed" 的条目;其他状态(如未来扩展的 "Failed")不影响重入检查。 + /// + public void OnLoad(SaveData data) + { + _completedChains.Clear(); + if (data?.EventChains?.ChainStates == null) return; + foreach (var kv in data.EventChains.ChainStates) + if (kv.Value == "Completed") + _completedChains.Add(kv.Key); + } + // ── 评估逻辑 ────────────────────────────────────────────────────── /// 收到新事件时立即评估所有链条件(无帧延迟)。 private void EvaluateAll() => DoEvaluateAll(); diff --git a/Assets/_Game/Scripts/World/FalseWall.cs b/Assets/_Game/Scripts/World/FalseWall.cs index 19a9e66..79cda34 100644 --- a/Assets/_Game/Scripts/World/FalseWall.cs +++ b/Assets/_Game/Scripts/World/FalseWall.cs @@ -1,6 +1,4 @@ using BaseGames.Combat; -using BaseGames.Core; -using BaseGames.Core.Save; using BaseGames.Feedback; using UnityEngine; @@ -8,10 +6,11 @@ namespace BaseGames.World { /// /// 假墙(秘密通道)。外观与普通墙相同,可通过攻击/接近揭示并穿越。 - /// 揭示后禁用碰撞体(不销毁),状态持久化到 WorldSaveData。 + /// 揭示后禁用碰撞体(不销毁),状态通过 WorldStateRegistry 持久化。 + /// WorldStateRegistrySaver(ISaveable)统一负责将 Door 分类写入存档。 /// [RequireComponent(typeof(Collider2D))] - public class FalseWall : MonoBehaviour, IDamageable, ISaveable + public class FalseWall : MonoBehaviour, IDamageable { public enum RevealCondition { Proximity, AttackOnce, AlwaysOpen } @@ -27,6 +26,10 @@ namespace BaseGames.World [SerializeField] private SpriteRenderer _renderer; [SerializeField] private SceneFeedback _revealFeedback; + [Header("World State")] + [Tooltip("绑定场景唯一的 WorldStateRegistry ScriptableObject,用于持久化揭示状态。")] + [SerializeField] private WorldStateRegistry _registry; + private bool _isRevealed; // ── IDamageable ─────────────────────────────────────────────────────── @@ -42,16 +45,6 @@ namespace BaseGames.World // ── Unity Lifecycle ─────────────────────────────────────────────────── - private void Awake() - { - ServiceLocator.GetOrDefault()?.Register(this); - } - - private void OnDestroy() - { - ServiceLocator.GetOrDefault()?.Unregister(this); - } - private void Start() { if (_revealCondition == RevealCondition.AlwaysOpen) @@ -60,21 +53,13 @@ namespace BaseGames.World _isRevealed = true; return; } - } - // ── ISaveable ───────────────────────────────────────────────────────── - public void OnSave(SaveData data) - { - if (_isRevealed && !string.IsNullOrEmpty(_wallId) - && !data.World.OpenedDoors.Contains(_wallId)) - data.World.OpenedDoors.Add(_wallId); - } - - public void OnLoad(SaveData data) - { - if (string.IsNullOrEmpty(_wallId)) return; - _isRevealed = data.World.OpenedDoors.Contains(_wallId); - if (_isRevealed) SetPassThroughImmediate(); + // 从 WorldStateRegistry 恢复揭示状态(注册表由 WorldStateRegistrySaver.OnLoad 填充) + if (!string.IsNullOrEmpty(_wallId) && (_registry?.IsDoorOpened(_wallId) ?? false)) + { + _isRevealed = true; + SetPassThroughImmediate(); + } } private void OnTriggerEnter2D(Collider2D other) @@ -90,6 +75,7 @@ namespace BaseGames.World private void Reveal() { _isRevealed = true; + _registry?.MarkDoorOpened(_wallId); _revealFeedback?.Play(); SetPassThroughImmediate(); } @@ -118,3 +104,4 @@ namespace BaseGames.World #endif } } +