370 lines
12 KiB
C#
370 lines
12 KiB
C#
|
|
using System;
|
|||
|
|
using System.Collections;
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using LLM;
|
|||
|
|
using Michsky.UI.Reach;
|
|||
|
|
using TMPro;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using UnityEngine.Serialization;
|
|||
|
|
using UnityEngine.SceneManagement;
|
|||
|
|
using UnityEngine.UI;
|
|||
|
|
|
|||
|
|
namespace UI.Ending
|
|||
|
|
{
|
|||
|
|
public class EndingUIScreenController : MonoBehaviour
|
|||
|
|
{
|
|||
|
|
[SerializeField] private bool debugLogs = true;
|
|||
|
|
|
|||
|
|
public enum EndingOutcome
|
|||
|
|
{
|
|||
|
|
Escaped,
|
|||
|
|
Fainted
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[Header("Scene")]
|
|||
|
|
[SerializeField] private int mainMenuSceneBuildIndex = 1;
|
|||
|
|
[SerializeField] private bool beginPreloadOnEnable = true;
|
|||
|
|
|
|||
|
|
[Header("UI")]
|
|||
|
|
[FormerlySerializedAs("summaryText")]
|
|||
|
|
[SerializeField] private TMP_Text summarayText;
|
|||
|
|
[SerializeField] private TMP_Text informationText;
|
|||
|
|
[SerializeField] private string requestingText = "正在生成总结...";
|
|||
|
|
|
|||
|
|
[Header("Information Text")]
|
|||
|
|
[SerializeField] private EndingOutcome outcome = EndingOutcome.Escaped;
|
|||
|
|
[SerializeField] private bool applyOutcomeTextOnEnable = true;
|
|||
|
|
[TextArea]
|
|||
|
|
[SerializeField] private string escapedInformationString = "(成功逃离时的提示词,自己填写)";
|
|||
|
|
[TextArea]
|
|||
|
|
[SerializeField] private string faintedInformationString = "(倒下/昏倒时的提示词,自己填写)";
|
|||
|
|
|
|||
|
|
[Header("Buttons")]
|
|||
|
|
[SerializeField] private ButtonManager returnToMenuButton;
|
|||
|
|
[SerializeField] private Button returnToMenuButtonUGUI;
|
|||
|
|
|
|||
|
|
[Header("AI Summary")]
|
|||
|
|
[SerializeField] private bool requestAISummaryOnEnable = true;
|
|||
|
|
[SerializeField] private LLMChatManager chatManager;
|
|||
|
|
[SerializeField] private int maxHistoryMessages = 30;
|
|||
|
|
[SerializeField] private bool includeKnownFactsFromContextMemory = true;
|
|||
|
|
[TextArea]
|
|||
|
|
[SerializeField] private string summarySystemPrompt = "你是一个游戏结局总结助手。你需要根据对话历史与已知事实,生成一段简短的结局总结,面向玩家。";
|
|||
|
|
[TextArea]
|
|||
|
|
[SerializeField] private string summaryUserPromptTemplate =
|
|||
|
|
"请为玩家生成结局总结。\n" +
|
|||
|
|
"要求:\n" +
|
|||
|
|
"- 80~160 字\n" +
|
|||
|
|
"- 只输出严格 JSON:{\"summary\":\"...\"}\n" +
|
|||
|
|
"- 不要输出代码块,不要多余字段\n\n" +
|
|||
|
|
"已知事实:\n{KNOWN_FACTS}\n\n" +
|
|||
|
|
"对话历史:\n{CHAT_HISTORY}\n";
|
|||
|
|
|
|||
|
|
[Header("AI Summary Debug")]
|
|||
|
|
[SerializeField] private ButtonManager debugSummaryButton;
|
|||
|
|
[SerializeField] private Button debugSummaryButtonUGUI;
|
|||
|
|
[TextArea]
|
|||
|
|
[SerializeField] private string debugSummaryString = "(这里是预设的 Debug 总结字符串)";
|
|||
|
|
|
|||
|
|
private AsyncOperation preloadOperation;
|
|||
|
|
private bool isWired;
|
|||
|
|
private Coroutine summaryCoroutine;
|
|||
|
|
|
|||
|
|
[Serializable]
|
|||
|
|
private class SummaryResponse
|
|||
|
|
{
|
|||
|
|
public string summary;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void Awake()
|
|||
|
|
{
|
|||
|
|
if (summarayText == null) { summarayText = FindTMPByNameHint("Summaray", "Summary"); }
|
|||
|
|
if (informationText == null) { informationText = FindTMPByNameHint("Information", "Info"); }
|
|||
|
|
|
|||
|
|
if (chatManager == null)
|
|||
|
|
{
|
|||
|
|
chatManager = FindObjectOfType<LLMChatManager>();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
WireOnce();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void OnEnable()
|
|||
|
|
{
|
|||
|
|
WireOnce();
|
|||
|
|
|
|||
|
|
if (beginPreloadOnEnable)
|
|||
|
|
{
|
|||
|
|
BeginPreloadMainMenu();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (applyOutcomeTextOnEnable)
|
|||
|
|
{
|
|||
|
|
ApplyOutcomeInformationText();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (requestAISummaryOnEnable)
|
|||
|
|
{
|
|||
|
|
GenerateSummaryWithAI();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void BeginPreloadMainMenu()
|
|||
|
|
{
|
|||
|
|
if (preloadOperation != null) { return; }
|
|||
|
|
|
|||
|
|
if (mainMenuSceneBuildIndex < 0 || mainMenuSceneBuildIndex >= SceneManager.sceneCountInBuildSettings)
|
|||
|
|
{
|
|||
|
|
if (debugLogs) Debug.Log($"[EndingUI] Invalid mainMenuSceneBuildIndex={mainMenuSceneBuildIndex}");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
StartCoroutine(PreloadRoutine());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private IEnumerator PreloadRoutine()
|
|||
|
|
{
|
|||
|
|
preloadOperation = SceneManager.LoadSceneAsync(mainMenuSceneBuildIndex);
|
|||
|
|
if (preloadOperation == null) { yield break; }
|
|||
|
|
|
|||
|
|
preloadOperation.allowSceneActivation = false;
|
|||
|
|
|
|||
|
|
while (preloadOperation.progress < 0.9f)
|
|||
|
|
{
|
|||
|
|
yield return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (debugLogs) Debug.Log("[EndingUI] Main menu preload ready (progress>=0.9).");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private bool IsPreloadReady()
|
|||
|
|
{
|
|||
|
|
return preloadOperation != null && preloadOperation.progress >= 0.9f;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void ReturnToMainMenu()
|
|||
|
|
{
|
|||
|
|
if (mainMenuSceneBuildIndex < 0 || mainMenuSceneBuildIndex >= SceneManager.sceneCountInBuildSettings)
|
|||
|
|
{
|
|||
|
|
if (debugLogs) Debug.Log($"[EndingUI] Invalid mainMenuSceneBuildIndex={mainMenuSceneBuildIndex}");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (preloadOperation != null)
|
|||
|
|
{
|
|||
|
|
preloadOperation.allowSceneActivation = true;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
SceneManager.LoadSceneAsync(mainMenuSceneBuildIndex);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void GenerateSummaryWithAI()
|
|||
|
|
{
|
|||
|
|
if (summarayText != null && !string.IsNullOrWhiteSpace(requestingText))
|
|||
|
|
{
|
|||
|
|
summarayText.text = requestingText;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (summaryCoroutine != null)
|
|||
|
|
{
|
|||
|
|
StopCoroutine(summaryCoroutine);
|
|||
|
|
summaryCoroutine = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
summaryCoroutine = StartCoroutine(GenerateSummaryRoutine());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private IEnumerator GenerateSummaryRoutine()
|
|||
|
|
{
|
|||
|
|
if (chatManager == null)
|
|||
|
|
{
|
|||
|
|
chatManager = FindObjectOfType<LLMChatManager>();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ILLMService service = GetActiveService();
|
|||
|
|
if (service == null)
|
|||
|
|
{
|
|||
|
|
ApplySummaryText("AI 服务未连接。");
|
|||
|
|
yield break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
string knownFacts = includeKnownFactsFromContextMemory ? BuildKnownFactsText(chatManager.contextMemory) : "无";
|
|||
|
|
string historyText = BuildHistoryText(chatManager.fullChatLog, maxHistoryMessages);
|
|||
|
|
string userPrompt = summaryUserPromptTemplate.Replace("{KNOWN_FACTS}", knownFacts).Replace("{CHAT_HISTORY}", historyText);
|
|||
|
|
|
|||
|
|
List<Message> messages = new List<Message>
|
|||
|
|
{
|
|||
|
|
new Message { role = "system", content = summarySystemPrompt },
|
|||
|
|
new Message { role = "user", content = userPrompt }
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
string result = null;
|
|||
|
|
bool ok = false;
|
|||
|
|
|
|||
|
|
yield return service.SendStatelessMessage(messages, (reply, success) =>
|
|||
|
|
{
|
|||
|
|
result = reply;
|
|||
|
|
ok = success;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (!ok)
|
|||
|
|
{
|
|||
|
|
ApplySummaryText(string.IsNullOrWhiteSpace(result) ? "AI 总结失败。" : result);
|
|||
|
|
yield break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
string summary = TryParseSummaryJson(result);
|
|||
|
|
ApplySummaryText(string.IsNullOrWhiteSpace(summary) ? result : summary);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void ApplyDebugSummary()
|
|||
|
|
{
|
|||
|
|
ApplySummaryText(debugSummaryString);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void SetOutcome(EndingOutcome newOutcome)
|
|||
|
|
{
|
|||
|
|
outcome = newOutcome;
|
|||
|
|
ApplyOutcomeInformationText();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void SetOutcomeEscaped()
|
|||
|
|
{
|
|||
|
|
SetOutcome(EndingOutcome.Escaped);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void SetOutcomeFainted()
|
|||
|
|
{
|
|||
|
|
SetOutcome(EndingOutcome.Fainted);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void ApplyOutcomeInformationText()
|
|||
|
|
{
|
|||
|
|
switch (outcome)
|
|||
|
|
{
|
|||
|
|
case EndingOutcome.Escaped:
|
|||
|
|
ApplyInformationText(escapedInformationString);
|
|||
|
|
break;
|
|||
|
|
case EndingOutcome.Fainted:
|
|||
|
|
ApplyInformationText(faintedInformationString);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void ApplySummaryText(string value)
|
|||
|
|
{
|
|||
|
|
if (summarayText == null) { return; }
|
|||
|
|
summarayText.text = value ?? string.Empty;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void ApplyInformationText(string value)
|
|||
|
|
{
|
|||
|
|
if (informationText == null) { return; }
|
|||
|
|
informationText.text = value ?? string.Empty;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private ILLMService GetActiveService()
|
|||
|
|
{
|
|||
|
|
if (chatManager == null) { return null; }
|
|||
|
|
|
|||
|
|
switch (chatManager.currentProvider)
|
|||
|
|
{
|
|||
|
|
case LLMChatManager.LLMProvider.DeepSeek:
|
|||
|
|
return chatManager.deepSeekService;
|
|||
|
|
case LLMChatManager.LLMProvider.Doubao:
|
|||
|
|
return chatManager.doubaoService;
|
|||
|
|
default:
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private string BuildKnownFactsText(ContextMemoryManager memory)
|
|||
|
|
{
|
|||
|
|
if (memory == null || memory.facts == null || memory.facts.Count == 0) { return "无"; }
|
|||
|
|
|
|||
|
|
System.Text.StringBuilder sb = new System.Text.StringBuilder();
|
|||
|
|
int count = Mathf.Min(30, memory.facts.Count);
|
|||
|
|
for (int i = 0; i < count; i++)
|
|||
|
|
{
|
|||
|
|
var fact = memory.facts[i];
|
|||
|
|
sb.Append(i + 1).Append(". [").Append(fact.key).Append("]: ").Append(fact.value).Append('\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return sb.ToString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private string BuildHistoryText(List<Message> fullLog, int maxCount)
|
|||
|
|
{
|
|||
|
|
if (fullLog == null || fullLog.Count == 0) { return "无"; }
|
|||
|
|
|
|||
|
|
int take = Mathf.Max(1, maxCount);
|
|||
|
|
int start = Mathf.Max(0, fullLog.Count - take);
|
|||
|
|
|
|||
|
|
System.Text.StringBuilder sb = new System.Text.StringBuilder();
|
|||
|
|
for (int i = start; i < fullLog.Count; i++)
|
|||
|
|
{
|
|||
|
|
var msg = fullLog[i];
|
|||
|
|
if (msg == null) { continue; }
|
|||
|
|
if (msg.role != "user" && msg.role != "assistant") { continue; }
|
|||
|
|
sb.Append(msg.role).Append(": ").Append(msg.content).Append('\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return sb.ToString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private string TryParseSummaryJson(string raw)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrWhiteSpace(raw)) { return null; }
|
|||
|
|
|
|||
|
|
string json = raw.Trim();
|
|||
|
|
json = json.Replace("```json", "").Replace("```", "").Trim();
|
|||
|
|
|
|||
|
|
SummaryResponse parsed = null;
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
parsed = JsonUtility.FromJson<SummaryResponse>(json);
|
|||
|
|
}
|
|||
|
|
catch
|
|||
|
|
{
|
|||
|
|
parsed = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (parsed == null || string.IsNullOrWhiteSpace(parsed.summary)) { return null; }
|
|||
|
|
return parsed.summary.Trim();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private TMP_Text FindTMPByNameHint(string hint1, string hint2)
|
|||
|
|
{
|
|||
|
|
TMP_Text[] all = GetComponentsInChildren<TMP_Text>(true);
|
|||
|
|
if (all == null || all.Length == 0) { return null; }
|
|||
|
|
|
|||
|
|
for (int i = 0; i < all.Length; i++)
|
|||
|
|
{
|
|||
|
|
var t = all[i];
|
|||
|
|
if (t == null) { continue; }
|
|||
|
|
string n = t.gameObject.name;
|
|||
|
|
if (!string.IsNullOrEmpty(n) && (n.IndexOf(hint1, StringComparison.OrdinalIgnoreCase) >= 0 || n.IndexOf(hint2, StringComparison.OrdinalIgnoreCase) >= 0))
|
|||
|
|
{
|
|||
|
|
return t;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return all[0];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void WireOnce()
|
|||
|
|
{
|
|||
|
|
if (isWired) { return; }
|
|||
|
|
|
|||
|
|
if (returnToMenuButton != null) { returnToMenuButton.onClick.AddListener(ReturnToMainMenu); }
|
|||
|
|
if (returnToMenuButtonUGUI != null) { returnToMenuButtonUGUI.onClick.AddListener(ReturnToMainMenu); }
|
|||
|
|
|
|||
|
|
if (debugSummaryButton != null) { debugSummaryButton.onClick.AddListener(ApplyDebugSummary); }
|
|||
|
|
if (debugSummaryButtonUGUI != null) { debugSummaryButtonUGUI.onClick.AddListener(ApplyDebugSummary); }
|
|||
|
|
|
|||
|
|
isWired = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|