117 lines
4.5 KiB
C#
117 lines
4.5 KiB
C#
using System;
|
||
using System.Threading.Tasks;
|
||
using UnityEngine;
|
||
using BaseGames.Core;
|
||
using BaseGames.Core.Events;
|
||
using BaseGames.Core.Save;
|
||
|
||
namespace BaseGames.World
|
||
{
|
||
/// <summary>
|
||
/// 存档点(也是复活点)。
|
||
/// 玩家互动后:恢复 HP → 写入存档(场景、出生点 ID、世界状态)→ 广播激活事件。
|
||
/// 传送点(快速旅行面板)是独立的 TeleportStation 组件,不在此处处理。
|
||
///
|
||
/// 继承 SaveableMonoBehaviour 以自动向 ISaveableRegistry 注册/注销,
|
||
/// 确保 GameSaveManager 在 SaveAsync/LoadAsync 时回调本组件。
|
||
///
|
||
/// OnSave 仅在 _isActivated=true 时写入复活字段,
|
||
/// 防止自动存档期间未激活的存档点覆盖玩家的正确复活位置。
|
||
/// </summary>
|
||
public class SavePoint : SaveableMonoBehaviour, IInteractable
|
||
{
|
||
[Header("Config")]
|
||
[SerializeField] private string _savePointId;
|
||
[SerializeField] private bool _restoreSpring = true;
|
||
|
||
[Header("Event Channels - Listen")]
|
||
[Tooltip("EVT_SceneLoaded — 用于追踪当前场景名,写入 Player.Scene")]
|
||
[SerializeField] private StringEventChannelSO _onSceneLoaded;
|
||
|
||
[Header("Event Channels - Raise")]
|
||
[SerializeField] private StringEventChannelSO _onSavePointActivated;
|
||
|
||
private bool _isActivated;
|
||
private string _currentSceneName;
|
||
|
||
private readonly CompositeDisposable _subs = new();
|
||
|
||
protected override void OnEnable()
|
||
{
|
||
base.OnEnable(); // 向 ISaveableRegistry 注册,确保 OnSave/OnLoad 被回调
|
||
_onSceneLoaded?.Subscribe(OnSceneLoaded).AddTo(_subs);
|
||
}
|
||
|
||
protected override void OnDisable()
|
||
{
|
||
base.OnDisable(); // 从 ISaveableRegistry 注销
|
||
_subs.Clear();
|
||
}
|
||
|
||
private void OnSceneLoaded(string sceneName) => _currentSceneName = sceneName;
|
||
|
||
// ── IInteractable ──────────────────────────────────────────────────────
|
||
public bool CanInteract => true;
|
||
public string InteractPrompt => _isActivated ? "休息" : "激活";
|
||
|
||
public void Interact(Transform player)
|
||
{
|
||
_isActivated = true;
|
||
|
||
var restorer = player.GetComponentInChildren<IRestoreOnSave>();
|
||
if (restorer != null)
|
||
{
|
||
restorer.FullRestore();
|
||
if (_restoreSpring) restorer.RestoreSpring();
|
||
}
|
||
|
||
_onSavePointActivated?.Raise(_savePointId);
|
||
|
||
// 触发存档:OnSave() 由 SaveAsync 回调所有 ISaveable,包含本组件
|
||
var svc = ServiceLocator.GetOrDefault<ISaveService>();
|
||
if (svc != null)
|
||
RunFireAndForget(svc.SaveAsync(svc.ActiveSlot));
|
||
else
|
||
Debug.LogWarning("[SavePoint] ISaveService 未注册,跳过存档。", this);
|
||
}
|
||
|
||
public void OnPlayerEnterRange(Transform player) { }
|
||
public void OnPlayerExitRange() { }
|
||
|
||
// ── ISaveable(通过 SaveableMonoBehaviour 注册)────────────────────────
|
||
public bool IsActivated => _isActivated;
|
||
public void SetActivated(bool val) => _isActivated = val;
|
||
|
||
public override void OnSave(SaveData data)
|
||
{
|
||
// 仅在本存档点已激活(玩家坐过)时更新复活字段。
|
||
// 自动存档也会触发此方法,必须守卫以防止覆盖玩家真正的复活位置。
|
||
if (!_isActivated) return;
|
||
|
||
if (!string.IsNullOrEmpty(_currentSceneName))
|
||
data.Player.Scene = _currentSceneName;
|
||
if (!string.IsNullOrEmpty(_savePointId))
|
||
data.Meta.SavePointId = _savePointId;
|
||
|
||
if (!string.IsNullOrEmpty(_savePointId)
|
||
&& !data.World.ActivatedSavePoints.Contains(_savePointId))
|
||
data.World.ActivatedSavePoints.Add(_savePointId);
|
||
}
|
||
|
||
public override void OnLoad(SaveData data)
|
||
{
|
||
_isActivated = !string.IsNullOrEmpty(_savePointId)
|
||
&& data.World.ActivatedSavePoints.Contains(_savePointId);
|
||
}
|
||
|
||
private static async void RunFireAndForget(Task task)
|
||
{
|
||
try { await task; }
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"[SavePoint] 存档失败: {e.Message}\n{e.StackTrace}");
|
||
}
|
||
}
|
||
}
|
||
}
|