diff --git a/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs b/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs new file mode 100644 index 0000000..ce8a3c4 --- /dev/null +++ b/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections; +using UnityEngine; + +namespace SecondSpawn.AI +{ + /// + /// Binds one scene NPC-like body to one Nakama actor profile. + /// Each bound actor has its own body, stats, traits, soul, memory, policy, + /// runtime counters, and activity log. + /// + [DisallowMultipleComponent] + public sealed class ActorProfileBinder : MonoBehaviour + { + [SerializeField] private bool _loadOnStart = true; + [SerializeField] private string _actorId = ""; + [SerializeField] private string _actorType = "npc"; + [SerializeField] private string _displayName = "Guide"; + [SerializeField] private string _archetypeId = "prototype-npc"; + [SerializeField] private string _visualPrefabKey = "prototype-npc"; + [SerializeField] private bool _applyDisplayNameToGameObject; + [SerializeField] private bool _seedMemoryOnStart = true; + [SerializeField] private string _seedMemoryKind = "system"; + [SerializeField, TextArea] private string _seedMemorySummary = + "This NPC-like body has an independent actor profile and memory."; + [SerializeField, Range(1, 10)] private int _seedMemoryImportance = 6; + + private SecondSpawnGatewayClient _gateway; + private Coroutine _loadRoutine; + private bool _isLoading; + + public ActorProfileDto CurrentProfile { get; private set; } + public bool IsLoading => _isLoading; + public bool IsReady => CurrentProfile != null; + public string ActorId => ResolveActorId(); + + public event Action ProfileLoaded; + + private void Awake() + { + _gateway = FindAnyObjectByType(); + } + + private void Start() + { + if (_loadOnStart) + { + Refresh(); + } + } + + public void Refresh() + { + if (_loadRoutine != null) + { + StopCoroutine(_loadRoutine); + _loadRoutine = null; + _isLoading = false; + } + + _loadRoutine = StartCoroutine(LoadActorProfileRoutine()); + } + + private IEnumerator LoadActorProfileRoutine() + { + _isLoading = true; + yield return LoadActorProfile(); + _isLoading = false; + _loadRoutine = null; + } + + private IEnumerator LoadActorProfile() + { + if (_gateway == null) + { + _gateway = FindAnyObjectByType(); + } + + if (_gateway == null) + { + Debug.LogWarning($"[ActorProfileBinder] No gateway client found for actor {ActorId}."); + yield break; + } + + ActorProfileDto profile = null; + string profileError = null; + yield return _gateway.GetNakamaActorProfile(BuildProfileRequest(), value => profile = value, error => profileError = error); + if (profile == null) + { + Debug.LogWarning($"[ActorProfileBinder] Actor profile load failed for {ActorId}: {profileError}"); + yield break; + } + + CurrentProfile = profile; + ApplyProfile(profile); + + if (_seedMemoryOnStart && !string.IsNullOrWhiteSpace(_seedMemorySummary)) + { + ActorProfileDto memoryProfile = null; + string memoryError = null; + yield return _gateway.AddNakamaActorMemory(new ActorMemoryAddRequestDto + { + actor_id = profile.actor_id, + kind = NormalizeIdentifier(_seedMemoryKind, "system"), + summary = _seedMemorySummary.Trim(), + importance = Mathf.Clamp(_seedMemoryImportance, 1, 10) + }, value => memoryProfile = value, error => memoryError = error); + + if (memoryProfile != null) + { + CurrentProfile = memoryProfile; + ApplyProfile(memoryProfile); + } + else if (!string.IsNullOrWhiteSpace(memoryError)) + { + Debug.LogWarning($"[ActorProfileBinder] Actor memory seed failed for {ActorId}: {memoryError}"); + } + } + + ProfileLoaded?.Invoke(CurrentProfile); + } + + private ActorProfileRequestDto BuildProfileRequest() + { + return new ActorProfileRequestDto + { + actor_id = ResolveActorId(), + actor_type = NormalizeIdentifier(_actorType, "npc"), + display_name = string.IsNullOrWhiteSpace(_displayName) ? gameObject.name : _displayName.Trim(), + archetype_id = string.IsNullOrWhiteSpace(_archetypeId) ? "prototype-npc" : _archetypeId.Trim(), + visual_prefab_key = string.IsNullOrWhiteSpace(_visualPrefabKey) ? "prototype-npc" : _visualPrefabKey.Trim() + }; + } + + private void ApplyProfile(ActorProfileDto profile) + { + if (profile == null) + { + return; + } + + if (!string.IsNullOrWhiteSpace(profile.actor_id)) + { + _actorId = profile.actor_id.Trim(); + } + + if (!string.IsNullOrWhiteSpace(profile.display_name)) + { + _displayName = profile.display_name.Trim(); + if (_applyDisplayNameToGameObject) + { + gameObject.name = _displayName; + } + } + } + + private string ResolveActorId() + { + if (!string.IsNullOrWhiteSpace(_actorId)) + { + return _actorId.Trim(); + } + + return string.IsNullOrWhiteSpace(gameObject.name) ? "npc-body" : gameObject.name.Trim(); + } + + private static string NormalizeIdentifier(string value, string fallback) + { + return string.IsNullOrWhiteSpace(value) ? fallback : value.Trim().ToLowerInvariant(); + } + } +} diff --git a/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs.meta b/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs.meta new file mode 100644 index 0000000..1b916a3 --- /dev/null +++ b/Unity/Assets/_SecondSpawn/Scripts/AI/ActorProfileBinder.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2bb44b8edfbf4b41a928f752d071c948