From 57eee1f9b551086b8e3fe0787274a58d1d5d189f Mon Sep 17 00:00:00 2001 From: JOY Date: Wed, 20 May 2026 22:54:39 +0700 Subject: [PATCH] fix: surface NPC dialog response lifecycle --- CHANGELOG.md | 2 + ROADMAP.md | 2 + .../Scripts/AI/PrototypeAgentBrain.cs | 2 + .../Scripts/AI/PrototypeNearbyNpcChatBox.cs | 75 ++++++++++++++++++- .../Scripts/UI/NearbyNpcChatPanel.cs | 8 ++ 5 files changed, 87 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 369510c9..43bb6436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ versioned release tag yet, so entries are organized as pre-alpha snapshots. ### Added +- Focused NPC dialog now reports response lifecycle in the UGUI chat panel: + waiting, retrying, answered, or failed after bounded DOS.AI retry. - Player-to-NPC chat now uses bounded direct-reply retry in Unity when the model path hits transient DOS.AI timeout or 5xx errors, while still suppressing deterministic fallback speech so only model-authored NPC lines diff --git a/ROADMAP.md b/ROADMAP.md index ef964a89..5f0c74e5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -141,6 +141,8 @@ Recommended views: - [x] Player-to-NPC dialog now retries transient DOS.AI timeout or 5xx failures a bounded number of times and keeps deterministic fallback speech out of the conversation UI. +- [x] Focused NPC dialog now shows response lifecycle feedback in the chat panel + so a failed DOS.AI reply is visible instead of looking like a silent NPC. - [ ] Real combat damage, enemy rewards, loot drops, quest progress, and player time-loot from other users are not implemented yet. diff --git a/Unity/Assets/_SecondSpawn/Scripts/AI/PrototypeAgentBrain.cs b/Unity/Assets/_SecondSpawn/Scripts/AI/PrototypeAgentBrain.cs index 49b4d7bb..3f0a5fe9 100644 --- a/Unity/Assets/_SecondSpawn/Scripts/AI/PrototypeAgentBrain.cs +++ b/Unity/Assets/_SecondSpawn/Scripts/AI/PrototypeAgentBrain.cs @@ -975,6 +975,7 @@ private IEnumerator PlayerChatResponseLoop() string decisionError = null; var request = BuildDecisionRequest(); SetBrainStatus("AI answering", new Color(0.72f, 0.82f, 0.95f), $"player chat attempt {_pendingPlayerChatAttemptCount}"); + PrototypeNearbyNpcChatBox.TryNotifyFocusedNpcResponseStarted(AgentId, DisplayName, _pendingPlayerChatAttemptCount); var stimulus = _pendingMessageFromNpc ? "npc_speech" : "player_chat"; Debug.Log($"[PrototypeAgentBrain] Player chat decision request agent={AgentId}, attempt={_pendingPlayerChatAttemptCount}, stimulus={stimulus}, allowed={string.Join(",", request.allowed ?? new string[0])}, message={request.world_snapshot?.last_player_message?.text}"); yield return NpcDecisionRequestScheduler.WaitForSlot( @@ -1017,6 +1018,7 @@ private IEnumerator PlayerChatResponseLoop() { Debug.LogWarning($"[PrototypeAgentBrain] Player chat response exhausted model attempts agent={AgentId}, attempts={_pendingPlayerChatAttemptCount}"); SetBrainStatus("AI no answer", new Color(1f, 0.62f, 0.16f), "player chat model attempts exhausted"); + PrototypeNearbyNpcChatBox.TryNotifyFocusedNpcResponseFailed(AgentId, DisplayName, "DOS.AI unavailable"); ClearPendingPlayerChat(); } diff --git a/Unity/Assets/_SecondSpawn/Scripts/AI/PrototypeNearbyNpcChatBox.cs b/Unity/Assets/_SecondSpawn/Scripts/AI/PrototypeNearbyNpcChatBox.cs index 1ed85fb8..88c0ee05 100644 --- a/Unity/Assets/_SecondSpawn/Scripts/AI/PrototypeNearbyNpcChatBox.cs +++ b/Unity/Assets/_SecondSpawn/Scripts/AI/PrototypeNearbyNpcChatBox.cs @@ -65,11 +65,24 @@ public static bool TryRouteFocusedNpcSpeech(string actorId, string displayName, return chat != null && chat.TryAddFocusedNpcSpeech(actorId, displayName, text); } + public static bool TryNotifyFocusedNpcResponseStarted(string actorId, string displayName, int attempt) + { + var chat = FindAnyObjectByType(); + return chat != null && chat.TryMarkFocusedNpcResponseStarted(actorId, displayName, attempt); + } + + public static bool TryNotifyFocusedNpcResponseFailed(string actorId, string displayName, string reason) + { + var chat = FindAnyObjectByType(); + return chat != null && chat.TryMarkFocusedNpcResponseFailed(actorId, displayName, reason); + } + public sealed class DialogueLine { public string speaker; public string text; public bool is_player; + public bool is_system; } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] @@ -210,7 +223,7 @@ private IEnumerator SendNearbyMessage(string message, List explicitActor _status = nearbyActorIds.Count == 0 ? "No nearby NPC heard that line." : IsFocusedNpcRecipient(nearbyActorIds) - ? $"Queued for {_focusedNpcDisplayName}." + ? $"Waiting for {_focusedNpcDisplayName}." : $"Queued for {nearbyActorIds.Count} nearby NPC{(nearbyActorIds.Count == 1 ? "" : "s")}."; Debug.Log($"[PrototypeNearbyNpcChatBox] Player message route recipients={nearbyActorIds.Count}, focused={IsFocusedNpcRecipient(nearbyActorIds)}, message={Shorten(message, 80)}"); @@ -667,7 +680,8 @@ private void AddHistory(string speaker, string text, bool isPlayer) { speaker = safeSpeaker, text = Shorten(text, 220), - is_player = isPlayer + is_player = isPlayer, + is_system = false }); while (_history.Count > Mathf.Max(1, _historyLimit)) @@ -697,6 +711,63 @@ private bool TryAddFocusedNpcSpeech(string actorId, string displayName, string t return true; } + private bool TryMarkFocusedNpcResponseStarted(string actorId, string displayName, int attempt) + { + if (!IsMatchingFocusedNpc(actorId)) + { + return false; + } + + FocusNpc(ResolveBrain(actorId)); + var safeName = string.IsNullOrWhiteSpace(displayName) ? FocusedNpcDisplayName : displayName.Trim(); + var suffix = attempt > 1 ? $" retry {attempt}" : ""; + _status = $"{safeName} is answering{suffix}."; + return true; + } + + private bool TryMarkFocusedNpcResponseFailed(string actorId, string displayName, string reason) + { + if (!IsMatchingFocusedNpc(actorId)) + { + return false; + } + + FocusNpc(ResolveBrain(actorId)); + var safeName = string.IsNullOrWhiteSpace(displayName) ? FocusedNpcDisplayName : displayName.Trim(); + var safeReason = string.IsNullOrWhiteSpace(reason) ? "model unavailable" : reason.Trim(); + _status = $"{safeName} could not answer: {safeReason}."; + AddSystemLine($"{safeName} could not reach DOS.AI after retrying. Try again in a moment."); + return true; + } + + private bool IsMatchingFocusedNpc(string actorId) + { + return IsFocusedNpcActive() && + !string.IsNullOrWhiteSpace(actorId) && + string.Equals(actorId.Trim(), _focusedNpcActorId, System.StringComparison.OrdinalIgnoreCase); + } + + private void AddSystemLine(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return; + } + + _dialogueLines.Add(new DialogueLine + { + speaker = "System", + text = Shorten(text, 220), + is_player = false, + is_system = true + }); + + while (_dialogueLines.Count > Mathf.Max(1, _historyLimit)) + { + _dialogueLines.RemoveAt(0); + } + } + private static bool ConsumeSubmitEventForChatField() { var current = Event.current; diff --git a/Unity/Assets/_SecondSpawn/Scripts/UI/NearbyNpcChatPanel.cs b/Unity/Assets/_SecondSpawn/Scripts/UI/NearbyNpcChatPanel.cs index 1e0d9aaa..af7abb1c 100644 --- a/Unity/Assets/_SecondSpawn/Scripts/UI/NearbyNpcChatPanel.cs +++ b/Unity/Assets/_SecondSpawn/Scripts/UI/NearbyNpcChatPanel.cs @@ -237,6 +237,8 @@ private void Render() builder.Append(line.text); builder.Append(':'); builder.Append(line.is_player); + builder.Append(':'); + builder.Append(line.is_system); } var state = builder.ToString(); @@ -327,6 +329,12 @@ private void RenderDialogueRows() var firstIsPlayer = _chat.DialogueLines[0].is_player; foreach (var line in _chat.DialogueLines) { + if (line.is_system) + { + CreateSystemRow(line.text); + continue; + } + CreateMessageRow(line, line.is_player == firstIsPlayer); } }