Initial Unity project commit
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using System.Collections.Generic;
|
||||
using LLM;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class ChatHistoryPanelController : MonoBehaviour
|
||||
{
|
||||
[Header("UI References")]
|
||||
public Transform historyContent; // 历史记录的父物体 (ScrollView Content)
|
||||
public GameObject messagePrefab; // 聊天气泡预设体,可复用 AssistPanel 的
|
||||
|
||||
[Header("Manager")]
|
||||
public LLMChatManager chatManager; // 引用 LLMChatManager
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (chatManager == null)
|
||||
{
|
||||
chatManager = FindObjectOfType<LLMChatManager>();
|
||||
}
|
||||
|
||||
if (chatManager != null)
|
||||
{
|
||||
RefreshHistoryUI();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[ChatHistoryPanelController] LLMChatManager is missing!");
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshHistoryUI()
|
||||
{
|
||||
if (historyContent == null || messagePrefab == null) return;
|
||||
|
||||
// 清理旧的记录
|
||||
foreach (Transform child in historyContent)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
|
||||
// 获取全部历史记录
|
||||
List<Message> fullLog = chatManager.fullChatLog;
|
||||
if (fullLog == null || fullLog.Count == 0) return;
|
||||
|
||||
// 遍历并生成气泡
|
||||
foreach (var msg in fullLog)
|
||||
{
|
||||
// 过滤掉 system 提示词等,只显示玩家和 AI 的对话
|
||||
if (msg.role == "user" || msg.role == "assistant")
|
||||
{
|
||||
bool isPlayer = (msg.role == "user");
|
||||
AddMessageToUI(msg.content, isPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
// 强制更新 Canvas 以确保布局正确,然后滚动到底部
|
||||
Canvas.ForceUpdateCanvases();
|
||||
ScrollRect scrollRect = historyContent.GetComponentInParent<ScrollRect>();
|
||||
if (scrollRect != null)
|
||||
{
|
||||
scrollRect.verticalNormalizedPosition = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMessageToUI(string text, bool isPlayer)
|
||||
{
|
||||
GameObject newMsg = Instantiate(messagePrefab, historyContent);
|
||||
TextMeshProUGUI tmp = newMsg.GetComponentInChildren<TextMeshProUGUI>();
|
||||
|
||||
if (tmp != null)
|
||||
{
|
||||
tmp.text = text;
|
||||
}
|
||||
|
||||
var aligner = newMsg.GetComponent<MessageAligner>();
|
||||
if (aligner != null)
|
||||
{
|
||||
aligner.SetAlignment(isPlayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b400e8d4667eed74380fa493df627247
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,369 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c785e7b5b8f56c45a40f8a6b5dff2b2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,412 @@
|
||||
using Inventory;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.InputSystem; // 引入 Input System
|
||||
using Player;
|
||||
|
||||
public class InventoryUI : MonoBehaviour
|
||||
{
|
||||
[Header("Player References")]
|
||||
public HandController handController; // 拖入 Player 身上的脚本
|
||||
public PlayerInput playerInput; // 拖入 Player 身上的 PlayerInput 组件
|
||||
|
||||
[Header("UI References")]
|
||||
public Transform hotbarPanel; // ���� HotbarPanel
|
||||
public GameObject slotPrefab; // ���������õ� Hotbar_Slot Prefab
|
||||
|
||||
[Header("Settings")]
|
||||
public Color selectedColor = new Color(0f, 1f, 1f, 0.5f); // ѡ��ʱ�ı߿���ɫ��������ɫ������
|
||||
public Color normalColor = new Color(0f, 1f, 1f, 0f); // δѡ��ʱ�ı߿���ɫ������
|
||||
|
||||
// �ڲ��ࣺ�����������ӵ� UI ���ã�����ÿ�� GetComponent
|
||||
private class SlotUI
|
||||
{
|
||||
public GameObject instance;
|
||||
public Image icon;
|
||||
public TextMeshProUGUI amountText;
|
||||
public Image frame; // �ñ߿���ɫ����ʾѡ��״̬
|
||||
}
|
||||
|
||||
private List<SlotUI> slots = new List<SlotUI>();
|
||||
private InputAction hotbar1Action;
|
||||
private InputAction hotbar2Action;
|
||||
private InputAction hotbar3Action;
|
||||
private InputAction hotbar4Action;
|
||||
private InputAction dropAction;
|
||||
|
||||
private System.Action<InputAction.CallbackContext> hotbar1Handler;
|
||||
private System.Action<InputAction.CallbackContext> hotbar2Handler;
|
||||
private System.Action<InputAction.CallbackContext> hotbar3Handler;
|
||||
private System.Action<InputAction.CallbackContext> hotbar4Handler;
|
||||
private System.Action<InputAction.CallbackContext> dropHandler;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (handController == null)
|
||||
{
|
||||
handController = FindObjectOfType<HandController>(true);
|
||||
}
|
||||
|
||||
if (playerInput == null)
|
||||
{
|
||||
var pc = FindObjectOfType<PlayerController>(true);
|
||||
if (pc != null)
|
||||
{
|
||||
playerInput = pc.GetComponent<PlayerInput>();
|
||||
}
|
||||
|
||||
if (playerInput == null)
|
||||
{
|
||||
playerInput = FindObjectOfType<PlayerInput>(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
TryInitializeUI();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (hotbar1Handler == null) hotbar1Handler = _ => SelectSlot(0);
|
||||
if (hotbar2Handler == null) hotbar2Handler = _ => SelectSlot(1);
|
||||
if (hotbar3Handler == null) hotbar3Handler = _ => SelectSlot(2);
|
||||
if (hotbar4Handler == null) hotbar4Handler = _ => SelectSlot(3);
|
||||
if (dropHandler == null) dropHandler = OnDropItem;
|
||||
|
||||
if (playerInput == null)
|
||||
{
|
||||
Debug.LogError("[InventoryUI] PlayerInput 未赋值,无法绑定 Hotbar/Drop 输入。请在 Inspector 里拖入玩家身上的 PlayerInput。");
|
||||
return;
|
||||
}
|
||||
|
||||
if (playerInput.currentActionMap != null && playerInput.currentActionMap.name != "Player")
|
||||
{
|
||||
Debug.LogWarning($"[InventoryUI] 当前 ActionMap 为 '{playerInput.currentActionMap.name}',Hotbar/Drop 可能不会触发。");
|
||||
}
|
||||
|
||||
if (playerInput != null)
|
||||
{
|
||||
// 订阅 Hotbar 切换事件
|
||||
// 注意:Action 名字必须和你 Input Actions 里的一模一样
|
||||
hotbar1Action = playerInput.actions["Hotbar1"];
|
||||
if (hotbar1Action != null) hotbar1Action.performed += hotbar1Handler;
|
||||
else Debug.LogError("[InventoryUI] 找不到名为 'Hotbar1' 的 Action,请检查 Input Actions。");
|
||||
|
||||
hotbar2Action = playerInput.actions["Hotbar2"];
|
||||
if (hotbar2Action != null) hotbar2Action.performed += hotbar2Handler;
|
||||
else Debug.LogError("[InventoryUI] 找不到名为 'Hotbar2' 的 Action,请检查 Input Actions。");
|
||||
|
||||
hotbar3Action = playerInput.actions["Hotbar3"];
|
||||
if (hotbar3Action != null) hotbar3Action.performed += hotbar3Handler;
|
||||
else Debug.LogError("[InventoryUI] 找不到名为 'Hotbar3' 的 Action,请检查 Input Actions。");
|
||||
|
||||
hotbar4Action = playerInput.actions["Hotbar4"];
|
||||
if (hotbar4Action != null) hotbar4Action.performed += hotbar4Handler;
|
||||
else Debug.LogError("[InventoryUI] 找不到名为 'Hotbar4' 的 Action,请检查 Input Actions。");
|
||||
|
||||
// 订阅丢弃事件
|
||||
dropAction = playerInput.actions["Drop"];
|
||||
if (dropAction != null) dropAction.performed += dropHandler;
|
||||
else Debug.LogError("[InventoryUI] 找不到名为 'Drop' 的 Action,请检查 Input Actions。");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// 取消订阅,防止内存泄漏
|
||||
if (hotbar1Action != null && hotbar1Handler != null) hotbar1Action.performed -= hotbar1Handler;
|
||||
if (hotbar2Action != null && hotbar2Handler != null) hotbar2Action.performed -= hotbar2Handler;
|
||||
if (hotbar3Action != null && hotbar3Handler != null) hotbar3Action.performed -= hotbar3Handler;
|
||||
if (hotbar4Action != null && hotbar4Handler != null) hotbar4Action.performed -= hotbar4Handler;
|
||||
if (dropAction != null && dropHandler != null) dropAction.performed -= dropHandler;
|
||||
|
||||
hotbar1Action = null;
|
||||
hotbar2Action = null;
|
||||
hotbar3Action = null;
|
||||
hotbar4Action = null;
|
||||
dropAction = null;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
TryInitializeUI();
|
||||
if (isInitialized)
|
||||
{
|
||||
RefreshUI();
|
||||
}
|
||||
}
|
||||
|
||||
private bool isInitialized;
|
||||
|
||||
private void TryInitializeUI()
|
||||
{
|
||||
if (isInitialized) return;
|
||||
if (InventorySystem.Instance == null) return;
|
||||
InitializeUI();
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
private void InitializeUI()
|
||||
{
|
||||
if (hotbarPanel == null || slotPrefab == null)
|
||||
{
|
||||
Debug.LogError("[InventoryUI] 缺少 HotbarPanel 或 SlotPrefab 引用!请在 Inspector 中赋值。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 清理现有的子物体(比如你在编辑器里复制的那 5 个测试格子)
|
||||
foreach (Transform child in hotbarPanel)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
slots.Clear();
|
||||
|
||||
// 2. 根据 InventorySystem 的最大槽位生成格子
|
||||
// 如果 InventorySystem 还没准备好,我们默认生成 5 个
|
||||
int slotCount = InventorySystem.Instance != null ? InventorySystem.Instance.maxSlots : 5;
|
||||
Debug.Log($"[InventoryUI] 正在生成 {slotCount} 个格子...");
|
||||
|
||||
for (int i = 0; i < slotCount; i++)
|
||||
{
|
||||
GameObject newSlot = Instantiate(slotPrefab, hotbarPanel);
|
||||
|
||||
SlotUI ui = new SlotUI();
|
||||
ui.instance = newSlot;
|
||||
|
||||
// 使用递归查找,这样即使层级稍微深一点也能找到
|
||||
ui.icon = FindChild<Image>(newSlot.transform, "Icon");
|
||||
ui.amountText = FindChild<TextMeshProUGUI>(newSlot.transform, "AmountText");
|
||||
ui.frame = FindChild<Image>(newSlot.transform, "Frame");
|
||||
|
||||
if (ui.icon == null) Debug.LogError($"[InventoryUI] 格子 {i} 找不到名为 'Icon' 的 Image 组件!");
|
||||
if (ui.amountText == null) Debug.LogError($"[InventoryUI] 格子 {i} 找不到名为 'AmountText' 的 TMP 组件!");
|
||||
|
||||
// 默认隐藏图标和文字,并重置颜色为不透明
|
||||
if (ui.icon != null)
|
||||
{
|
||||
ui.icon.color = Color.white;
|
||||
ui.icon.sprite = null;
|
||||
ui.icon.gameObject.SetActive(false);
|
||||
}
|
||||
if (ui.amountText != null) ui.amountText.gameObject.SetActive(false);
|
||||
|
||||
slots.Add(ui);
|
||||
}
|
||||
|
||||
// 3. 初始化默认选择第一个格子
|
||||
SelectSlot(0);
|
||||
}
|
||||
|
||||
// 辅助方法:递归查找子物体组件
|
||||
private T FindChild<T>(Transform parent, string name) where T : Component
|
||||
{
|
||||
foreach (Transform child in parent)
|
||||
{
|
||||
if (child.name == name) return child.GetComponent<T>();
|
||||
T found = FindChild<T>(child, name);
|
||||
if (found != null) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 记录上一次当前选中格子的物品数据,用于检测是否捡到了新东西
|
||||
private ItemData lastSelectedItemData = null;
|
||||
|
||||
private void RefreshUI()
|
||||
{
|
||||
if (InventorySystem.Instance == null) return;
|
||||
|
||||
List<InventoryItem> items = InventorySystem.Instance.inventory;
|
||||
|
||||
// 检查当前选中格子的物品是否发生了变化
|
||||
if (currentSelection >= 0 && currentSelection < items.Count)
|
||||
{
|
||||
InventoryItem currentItem = items[currentSelection];
|
||||
ItemData currentData = currentItem?.data;
|
||||
|
||||
// 如果物品数据变了(比如从 null 变成了 Apple,或者从 Apple 换成了 Bottle)
|
||||
// 并且当前没有强制收手,就自动刷新手中的模型
|
||||
if (currentData != lastSelectedItemData)
|
||||
{
|
||||
lastSelectedItemData = currentData;
|
||||
if (!isHandHidden)
|
||||
{
|
||||
UpdateHandItem();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < slots.Count; i++)
|
||||
{
|
||||
InventoryItem item = i < items.Count ? items[i] : null;
|
||||
ItemData data = item != null ? item.data : null;
|
||||
Sprite iconSprite = data != null ? data.icon : null;
|
||||
|
||||
bool hasRenderableItem = data != null && iconSprite != null;
|
||||
|
||||
if (slots[i].icon != null)
|
||||
{
|
||||
if (hasRenderableItem)
|
||||
{
|
||||
slots[i].icon.sprite = iconSprite;
|
||||
slots[i].icon.color = Color.white;
|
||||
slots[i].icon.gameObject.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
slots[i].icon.sprite = null;
|
||||
slots[i].icon.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (slots[i].amountText != null)
|
||||
{
|
||||
if (data != null && item != null && item.stackSize > 1)
|
||||
{
|
||||
slots[i].amountText.gameObject.SetActive(true);
|
||||
slots[i].amountText.text = item.stackSize.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
slots[i].amountText.text = string.Empty;
|
||||
slots[i].amountText.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 丢弃物品的逻辑桩
|
||||
private void OnDropItem(InputAction.CallbackContext context)
|
||||
{
|
||||
if (currentSelection < 0 || InventorySystem.Instance == null) return;
|
||||
|
||||
// 检查当前格子有没有东西
|
||||
if (currentSelection < InventorySystem.Instance.inventory.Count)
|
||||
{
|
||||
var item = InventorySystem.Instance.inventory[currentSelection];
|
||||
if (item != null && item.data != null)
|
||||
{
|
||||
Debug.Log($"[Input] 按下了 G 键,准备丢弃: {item.data.name}");
|
||||
|
||||
// 1. 在玩家前方生成掉落物
|
||||
if (item.data.pickupPrefab != null)
|
||||
{
|
||||
// 优先使用 HandController 的位置(即 Player 位置)作为基准
|
||||
// 如果能获取到具体的 handPosition(手的位置)则更好,否则用 Player 身体位置
|
||||
Transform origin = handController != null ? handController.transform : transform;
|
||||
|
||||
// 如果 handController 有定义 handPosition,尽量用它,因为它通常在摄像机附近
|
||||
if (handController != null && handController.handPosition != null)
|
||||
{
|
||||
origin = handController.handPosition;
|
||||
}
|
||||
|
||||
// 在起点前方 0.5 米处生成(避免穿模)
|
||||
Vector3 spawnPos = origin.position + origin.forward * 0.5f;
|
||||
|
||||
GameObject droppedObj = Instantiate(item.data.pickupPrefab, spawnPos, Quaternion.identity);
|
||||
|
||||
// 1.5 如果有 ItemPhysicsState,强制解除可能存在的 startKinematic 锁定
|
||||
var physState = droppedObj.GetComponent<Project.Interaction.ItemPhysicsState>();
|
||||
if (physState != null)
|
||||
{
|
||||
physState.ReleaseKinematic();
|
||||
}
|
||||
|
||||
// 2. 如果有刚体,给它一个向前的力(扔出去的感觉)
|
||||
Rigidbody rb = droppedObj.GetComponent<Rigidbody>();
|
||||
if (rb != null)
|
||||
{
|
||||
// 确保物理完全启用(作为最后保障)
|
||||
rb.isKinematic = false;
|
||||
rb.useGravity = true;
|
||||
|
||||
// 随机一点旋转,更自然
|
||||
rb.AddTorque(Random.insideUnitSphere * 5f);
|
||||
// 使用 origin.forward 确保是向当前朝向扔出
|
||||
rb.AddForce(origin.forward * 4f + Vector3.up * 1.5f, ForceMode.Impulse);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[Inventory] 物品 {item.data.name} 缺少 PickupPrefab,无法在场景中生成掉落物!只会在背包中移除。");
|
||||
}
|
||||
|
||||
// 3. 从背包中移除 1 个
|
||||
InventorySystem.Instance.RemoveFromSlot(currentSelection, 1);
|
||||
|
||||
// 4. 手部模型更新会由 RefreshUI 自动处理
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[Input] 当前格子是空的,无法丢弃。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int currentSelection = -1;
|
||||
private bool isHandHidden = false; // 记录当前是否强制收手
|
||||
|
||||
private void SelectSlot(int index)
|
||||
{
|
||||
if (index < 0 || index >= slots.Count) return;
|
||||
|
||||
// 如果再次按下当前选中的格子 -> 切换收手/拿出状态
|
||||
if (index == currentSelection)
|
||||
{
|
||||
isHandHidden = !isHandHidden;
|
||||
UpdateHandItem(); // 更新手部显示
|
||||
return;
|
||||
}
|
||||
|
||||
// 切换到了新格子
|
||||
currentSelection = index;
|
||||
isHandHidden = false; // 重置收手状态,默认拿出新物品
|
||||
|
||||
// 同步选中状态给 InventorySystem
|
||||
if (InventorySystem.Instance != null)
|
||||
{
|
||||
InventorySystem.Instance.selectedSlotIndex = index;
|
||||
}
|
||||
|
||||
// 更新高亮显示
|
||||
for (int i = 0; i < slots.Count; i++)
|
||||
{
|
||||
if (slots[i].frame != null)
|
||||
{
|
||||
slots[i].frame.color = (i == index) ? selectedColor : normalColor;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateHandItem();
|
||||
Debug.Log($"选择了第 {index + 1} 个格子");
|
||||
}
|
||||
|
||||
// 独立出来的更新手部物品方法
|
||||
private void UpdateHandItem()
|
||||
{
|
||||
if (handController == null || InventorySystem.Instance == null) return;
|
||||
|
||||
// 如果强制收手,直接传 null
|
||||
if (isHandHidden)
|
||||
{
|
||||
handController.EquipItem(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 否则尝试装备当前选中格子的物品
|
||||
if (currentSelection >= 0 && currentSelection < InventorySystem.Instance.inventory.Count)
|
||||
{
|
||||
var item = InventorySystem.Instance.inventory[currentSelection];
|
||||
handController.EquipItem(item != null ? item.data : null);
|
||||
}
|
||||
else
|
||||
{
|
||||
handController.EquipItem(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43d09e0bf49be5745adefcff6e625d20
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,339 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
[RequireComponent(typeof(VerticalLayoutGroup))]
|
||||
public class MessageAligner : MonoBehaviour
|
||||
{
|
||||
[Header("Bubble")]
|
||||
public Image bubbleBackground;
|
||||
public Sprite playerBubbleSprite;
|
||||
public Sprite aiBubbleSprite;
|
||||
public Color playerColor = new Color(0.2f, 0.6f, 1f);
|
||||
public Color aiColor = new Color(0.2f, 0.2f, 0.2f);
|
||||
public Color playerTextColor = Color.white;
|
||||
public Color aiTextColor = Color.white;
|
||||
|
||||
[Header("Avatar Row")]
|
||||
public HorizontalLayoutGroup rowLayout;
|
||||
public GameObject avatarObject;
|
||||
public Image avatarImage;
|
||||
public Sprite playerAvatarSprite;
|
||||
public Sprite aiAvatarSprite;
|
||||
public Color playerAvatarColor = Color.white;
|
||||
public Color aiAvatarColor = Color.white;
|
||||
public bool showPlayerAvatar = true;
|
||||
public bool showAiAvatar = true;
|
||||
public float rowHorizontalPadding = 20f;
|
||||
public float rowSpacingWhenAvatarVisible = 14f;
|
||||
public float rowSpacingWhenAvatarHidden = 0f;
|
||||
public float rowMinHeight = 80f;
|
||||
|
||||
[Header("Layout")]
|
||||
[Tooltip("Hard cap for bubble width before wrapping kicks in.")]
|
||||
public float maxBubbleWidth = 1000f;
|
||||
public bool clampToViewportWidth = true;
|
||||
[Range(0.1f, 1f)] public float viewportWidthRatio = 0.7f;
|
||||
|
||||
private TextMeshProUGUI textComponent;
|
||||
private Sprite fallbackBubbleSprite;
|
||||
private Sprite fallbackAvatarSprite;
|
||||
private bool lastIsPlayer;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ResolveReferences();
|
||||
CacheFallbacks();
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
ResolveReferences();
|
||||
CacheFallbacks();
|
||||
}
|
||||
|
||||
public void SetAlignment(bool isPlayer)
|
||||
{
|
||||
lastIsPlayer = isPlayer;
|
||||
|
||||
ResolveReferences();
|
||||
CacheFallbacks();
|
||||
|
||||
ApplyRootLayout();
|
||||
ApplyAvatarStyle(isPlayer);
|
||||
ApplyRowLayout(isPlayer);
|
||||
ApplyBubbleStyle(isPlayer);
|
||||
ApplyTextStyle(isPlayer);
|
||||
ApplyWidthLimit();
|
||||
RebuildLayout();
|
||||
}
|
||||
|
||||
public void Reapply()
|
||||
{
|
||||
SetAlignment(lastIsPlayer);
|
||||
}
|
||||
|
||||
private void ResolveReferences()
|
||||
{
|
||||
if (bubbleBackground == null)
|
||||
{
|
||||
TextMeshProUGUI tmp = GetComponentInChildren<TextMeshProUGUI>(true);
|
||||
if (tmp != null)
|
||||
{
|
||||
bubbleBackground = tmp.GetComponentInParent<Image>();
|
||||
}
|
||||
}
|
||||
|
||||
if (bubbleBackground != null && textComponent == null)
|
||||
{
|
||||
textComponent = bubbleBackground.GetComponentInChildren<TextMeshProUGUI>(true);
|
||||
}
|
||||
|
||||
if (rowLayout == null && bubbleBackground != null && bubbleBackground.transform.parent != null)
|
||||
{
|
||||
rowLayout = bubbleBackground.transform.parent.GetComponent<HorizontalLayoutGroup>();
|
||||
}
|
||||
|
||||
if (avatarObject == null && rowLayout != null)
|
||||
{
|
||||
foreach (Transform child in rowLayout.transform)
|
||||
{
|
||||
if (bubbleBackground == null || child.gameObject != bubbleBackground.gameObject)
|
||||
{
|
||||
avatarObject = child.gameObject;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (avatarImage == null && avatarObject != null)
|
||||
{
|
||||
avatarImage = avatarObject.GetComponent<Image>();
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheFallbacks()
|
||||
{
|
||||
if (bubbleBackground != null)
|
||||
{
|
||||
if (fallbackBubbleSprite == null)
|
||||
{
|
||||
fallbackBubbleSprite = bubbleBackground.sprite;
|
||||
}
|
||||
|
||||
if (aiBubbleSprite == null) aiBubbleSprite = fallbackBubbleSprite;
|
||||
if (playerBubbleSprite == null) playerBubbleSprite = fallbackBubbleSprite;
|
||||
}
|
||||
|
||||
if (avatarImage != null)
|
||||
{
|
||||
if (fallbackAvatarSprite == null)
|
||||
{
|
||||
fallbackAvatarSprite = avatarImage.sprite;
|
||||
}
|
||||
|
||||
if (aiAvatarSprite == null) aiAvatarSprite = fallbackAvatarSprite;
|
||||
if (playerAvatarSprite == null) playerAvatarSprite = fallbackAvatarSprite;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyRootLayout()
|
||||
{
|
||||
VerticalLayoutGroup layoutGroup = GetComponent<VerticalLayoutGroup>();
|
||||
if (layoutGroup == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int horizontalPadding = Mathf.RoundToInt(Mathf.Max(0f, rowHorizontalPadding));
|
||||
|
||||
layoutGroup.childControlWidth = true;
|
||||
layoutGroup.childForceExpandWidth = true;
|
||||
layoutGroup.childControlHeight = true;
|
||||
layoutGroup.childForceExpandHeight = false;
|
||||
layoutGroup.childAlignment = TextAnchor.UpperLeft;
|
||||
layoutGroup.padding.left = horizontalPadding;
|
||||
layoutGroup.padding.right = horizontalPadding;
|
||||
}
|
||||
|
||||
private void ApplyRowLayout(bool isPlayer)
|
||||
{
|
||||
if (rowLayout == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LayoutElement rowElement = rowLayout.GetComponent<LayoutElement>();
|
||||
if (rowElement == null)
|
||||
{
|
||||
rowElement = rowLayout.gameObject.AddComponent<LayoutElement>();
|
||||
}
|
||||
|
||||
rowElement.flexibleWidth = 1f;
|
||||
if (rowMinHeight > 0f)
|
||||
{
|
||||
rowElement.minHeight = rowMinHeight;
|
||||
}
|
||||
|
||||
bool avatarVisible = avatarObject != null && avatarObject.activeSelf;
|
||||
|
||||
rowLayout.childAlignment = isPlayer ? TextAnchor.LowerRight : TextAnchor.LowerLeft;
|
||||
rowLayout.reverseArrangement = isPlayer && avatarVisible;
|
||||
rowLayout.spacing = avatarVisible ? rowSpacingWhenAvatarVisible : rowSpacingWhenAvatarHidden;
|
||||
rowLayout.childControlWidth = false;
|
||||
rowLayout.childControlHeight = false;
|
||||
rowLayout.childForceExpandWidth = false;
|
||||
rowLayout.childForceExpandHeight = false;
|
||||
}
|
||||
|
||||
private void ApplyAvatarStyle(bool isPlayer)
|
||||
{
|
||||
bool shouldShowAvatar = isPlayer ? showPlayerAvatar : showAiAvatar;
|
||||
|
||||
if (avatarObject != null)
|
||||
{
|
||||
avatarObject.SetActive(shouldShowAvatar);
|
||||
}
|
||||
|
||||
if (!shouldShowAvatar || avatarImage == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
avatarImage.sprite = ResolveAvatarSprite(isPlayer);
|
||||
avatarImage.color = isPlayer ? playerAvatarColor : aiAvatarColor;
|
||||
avatarImage.preserveAspect = true;
|
||||
}
|
||||
|
||||
private void ApplyBubbleStyle(bool isPlayer)
|
||||
{
|
||||
if (bubbleBackground == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bubbleBackground.sprite = ResolveBubbleSprite(isPlayer);
|
||||
bubbleBackground.color = isPlayer ? playerColor : aiColor;
|
||||
}
|
||||
|
||||
private void ApplyTextStyle(bool isPlayer)
|
||||
{
|
||||
if (textComponent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
textComponent.color = isPlayer ? playerTextColor : aiTextColor;
|
||||
}
|
||||
|
||||
private void ApplyWidthLimit()
|
||||
{
|
||||
if (textComponent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float effectiveMaxWidth = maxBubbleWidth;
|
||||
if (clampToViewportWidth)
|
||||
{
|
||||
float viewportWidth = GetViewportWidth();
|
||||
if (viewportWidth > 0f)
|
||||
{
|
||||
float viewportMaxWidth = viewportWidth * viewportWidthRatio;
|
||||
if (viewportMaxWidth > 0f)
|
||||
{
|
||||
effectiveMaxWidth = effectiveMaxWidth <= 0f
|
||||
? viewportMaxWidth
|
||||
: Mathf.Min(effectiveMaxWidth, viewportMaxWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LayoutElement textLayout = textComponent.GetComponent<LayoutElement>();
|
||||
if (textLayout == null)
|
||||
{
|
||||
textLayout = textComponent.gameObject.AddComponent<LayoutElement>();
|
||||
}
|
||||
|
||||
Vector2 preferredSize =
|
||||
textComponent.GetPreferredValues(textComponent.text, float.PositiveInfinity, float.PositiveInfinity);
|
||||
|
||||
if (effectiveMaxWidth > 0f && preferredSize.x > effectiveMaxWidth)
|
||||
{
|
||||
textLayout.enabled = true;
|
||||
textLayout.preferredWidth = effectiveMaxWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
textLayout.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Sprite ResolveBubbleSprite(bool isPlayer)
|
||||
{
|
||||
if (isPlayer)
|
||||
{
|
||||
if (playerBubbleSprite != null) return playerBubbleSprite;
|
||||
if (aiBubbleSprite != null) return aiBubbleSprite;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (aiBubbleSprite != null) return aiBubbleSprite;
|
||||
if (playerBubbleSprite != null) return playerBubbleSprite;
|
||||
}
|
||||
|
||||
return fallbackBubbleSprite;
|
||||
}
|
||||
|
||||
private Sprite ResolveAvatarSprite(bool isPlayer)
|
||||
{
|
||||
if (isPlayer)
|
||||
{
|
||||
if (playerAvatarSprite != null) return playerAvatarSprite;
|
||||
if (aiAvatarSprite != null) return aiAvatarSprite;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (aiAvatarSprite != null) return aiAvatarSprite;
|
||||
if (playerAvatarSprite != null) return playerAvatarSprite;
|
||||
}
|
||||
|
||||
return fallbackAvatarSprite;
|
||||
}
|
||||
|
||||
private void RebuildLayout()
|
||||
{
|
||||
if (rowLayout != null)
|
||||
{
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(rowLayout.GetComponent<RectTransform>());
|
||||
}
|
||||
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(GetComponent<RectTransform>());
|
||||
|
||||
RectTransform parentRt = transform.parent as RectTransform;
|
||||
if (parentRt != null)
|
||||
{
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(parentRt);
|
||||
}
|
||||
}
|
||||
|
||||
private float GetViewportWidth()
|
||||
{
|
||||
ScrollRect scrollRect = GetComponentInParent<ScrollRect>();
|
||||
if (scrollRect != null)
|
||||
{
|
||||
RectTransform viewport = scrollRect.viewport != null
|
||||
? scrollRect.viewport
|
||||
: scrollRect.GetComponent<RectTransform>();
|
||||
if (viewport != null) return viewport.rect.width;
|
||||
}
|
||||
|
||||
RectTransform parentRt = transform.parent as RectTransform;
|
||||
if (parentRt != null) return parentRt.rect.width;
|
||||
|
||||
return 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f55d373ce5c2c6141950213635a2b3d9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f8e7d6c5b4a39281706f5e4d3c2b1a0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
using Michsky.UI.Reach;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace UI.Narration
|
||||
{
|
||||
public class NarrationPanelView : MonoBehaviour
|
||||
{
|
||||
[Header("Root")]
|
||||
[SerializeField] private GameObject root;
|
||||
|
||||
[Header("Text")]
|
||||
[SerializeField] private TMP_Text narrationText;
|
||||
|
||||
[Header("Continue Button")]
|
||||
[SerializeField] private ButtonManager continueButton;
|
||||
[SerializeField] private Button fallbackContinueButton;
|
||||
[SerializeField] private string continueLabel = "继续";
|
||||
[SerializeField] private string finishLabel = "确认";
|
||||
|
||||
public UnityEvent onContinue = new UnityEvent();
|
||||
|
||||
UnityAction onContinueAction;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (root == null) { root = gameObject; }
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
BindButton();
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
UnbindButton();
|
||||
}
|
||||
|
||||
void BindButton()
|
||||
{
|
||||
UnbindButton();
|
||||
onContinueAction = RaiseContinue;
|
||||
|
||||
if (continueButton != null)
|
||||
{
|
||||
continueButton.onClick.AddListener(onContinueAction);
|
||||
}
|
||||
else if (fallbackContinueButton != null)
|
||||
{
|
||||
fallbackContinueButton.onClick.AddListener(onContinueAction);
|
||||
}
|
||||
}
|
||||
|
||||
void UnbindButton()
|
||||
{
|
||||
if (onContinueAction == null) { return; }
|
||||
|
||||
if (continueButton != null)
|
||||
{
|
||||
continueButton.onClick.RemoveListener(onContinueAction);
|
||||
}
|
||||
|
||||
if (fallbackContinueButton != null)
|
||||
{
|
||||
fallbackContinueButton.onClick.RemoveListener(onContinueAction);
|
||||
}
|
||||
|
||||
onContinueAction = null;
|
||||
}
|
||||
|
||||
void RaiseContinue()
|
||||
{
|
||||
onContinue.Invoke();
|
||||
}
|
||||
|
||||
public void Show()
|
||||
{
|
||||
if (root != null) { root.SetActive(true); }
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
if (root != null) { root.SetActive(false); }
|
||||
}
|
||||
|
||||
public void SetText(string text)
|
||||
{
|
||||
if (narrationText != null) { narrationText.text = text; }
|
||||
}
|
||||
|
||||
public void SetIsLastPage(bool isLastPage)
|
||||
{
|
||||
string label = isLastPage ? finishLabel : continueLabel;
|
||||
|
||||
if (continueButton != null)
|
||||
{
|
||||
continueButton.SetText(label);
|
||||
}
|
||||
else if (fallbackContinueButton != null)
|
||||
{
|
||||
var tmp = fallbackContinueButton.GetComponentInChildren<TMP_Text>(true);
|
||||
if (tmp != null) { tmp.text = label; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a1b2c3d4e5f60718293a4b5c6d7e8f9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dd43061e5b0b61949a78227735dbf2bb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,96 @@
|
||||
using UnityEngine;
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem;
|
||||
#endif
|
||||
|
||||
namespace UI.PanelStack
|
||||
{
|
||||
public class PlayerInputActionRouter : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private bool debugLogs = true;
|
||||
[SerializeField] private string pauseActionName = "Pause";
|
||||
[SerializeField] private string cancelActionName = "Cancel";
|
||||
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
private PlayerInput playerInput;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
TryBind();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
Unbind();
|
||||
}
|
||||
|
||||
private void TryBind()
|
||||
{
|
||||
playerInput = FindPrimaryPlayerInput();
|
||||
if (playerInput == null)
|
||||
{
|
||||
if (debugLogs) Debug.Log("[PlayerInputActionRouter] No PlayerInput found; input routing disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
playerInput.onActionTriggered += OnActionTriggered;
|
||||
|
||||
if (debugLogs)
|
||||
{
|
||||
string map = playerInput.currentActionMap != null ? playerInput.currentActionMap.name : "(null)";
|
||||
bool hasPause = playerInput.actions != null && playerInput.actions.FindAction(pauseActionName, false) != null;
|
||||
bool hasCancel = playerInput.actions != null && playerInput.actions.FindAction(cancelActionName, false) != null;
|
||||
Debug.Log($"[PlayerInputActionRouter] Bound. currentMap={map}, hasPause={hasPause}, hasCancel={hasCancel}");
|
||||
}
|
||||
}
|
||||
|
||||
private static PlayerInput FindPrimaryPlayerInput()
|
||||
{
|
||||
Player.PlayerController playerController = FindObjectOfType<Player.PlayerController>(true);
|
||||
if (playerController != null)
|
||||
{
|
||||
PlayerInput playerInput = playerController.GetComponentInChildren<PlayerInput>(true);
|
||||
if (playerInput != null) { return playerInput; }
|
||||
}
|
||||
|
||||
return FindObjectOfType<PlayerInput>(true);
|
||||
}
|
||||
|
||||
private void Unbind()
|
||||
{
|
||||
if (playerInput != null)
|
||||
{
|
||||
playerInput.onActionTriggered -= OnActionTriggered;
|
||||
}
|
||||
playerInput = null;
|
||||
}
|
||||
|
||||
private void OnActionTriggered(InputAction.CallbackContext ctx)
|
||||
{
|
||||
if (ctx.action == null) { return; }
|
||||
if (ctx.performed == false) { return; }
|
||||
if (UIPanelStack.Instance == null) { return; }
|
||||
|
||||
string actionName = ctx.action.name;
|
||||
if (actionName == cancelActionName)
|
||||
{
|
||||
bool closed = UIPanelStack.Instance.CloseTop();
|
||||
if (debugLogs) Debug.Log($"[PlayerInputActionRouter] {cancelActionName} performed. closedTop={closed}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actionName == pauseActionName)
|
||||
{
|
||||
if (UIPanelStack.Instance.IsAnyOpen())
|
||||
{
|
||||
if (debugLogs) Debug.Log($"[PlayerInputActionRouter] {pauseActionName} performed but stack not empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
bool opened = UIPanelStack.Instance.TryOpenOnEscapeWhenEmpty();
|
||||
if (debugLogs) Debug.Log($"[PlayerInputActionRouter] {pauseActionName} performed. openedMenu={opened}");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dbb84ff6c82d9df468c6cc9c439f9f95
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,25 @@
|
||||
using UI.PanelStack;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UI.PanelStack
|
||||
{
|
||||
public class UIActionMapRouter : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private bool debugLogs = true;
|
||||
|
||||
public void OnUICancel()
|
||||
{
|
||||
if (UIPanelStack.Instance == null) { return; }
|
||||
bool closed = UIPanelStack.Instance.CloseTop();
|
||||
if (debugLogs) Debug.Log($"[UIActionMapRouter] UI/Cancel triggered. closedTop={closed}");
|
||||
}
|
||||
|
||||
public void OnUIPause()
|
||||
{
|
||||
if (UIPanelStack.Instance == null) { return; }
|
||||
if (UIPanelStack.Instance.IsAnyOpen()) { return; }
|
||||
bool opened = UIPanelStack.Instance.TryOpenOnEscapeWhenEmpty();
|
||||
if (debugLogs) Debug.Log($"[UIActionMapRouter] UI/Pause triggered. openedMenu={opened}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f71ed2608c4634c41821d378c37ae68f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace UI.PanelStack
|
||||
{
|
||||
public enum UIPanelKind
|
||||
{
|
||||
Settings = 0,
|
||||
Modal = 1,
|
||||
Pause = 2,
|
||||
Custom = 100
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c207c3d0faf77841a5b735083d1d4b0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UI.PanelStack
|
||||
{
|
||||
public class UIPanelStack : MonoBehaviour
|
||||
{
|
||||
public static UIPanelStack Instance { get; private set; }
|
||||
|
||||
private readonly List<Entry> stack = new List<Entry>();
|
||||
private Func<bool> escapeOpenHandler;
|
||||
[SerializeField] private bool debugLogs = true;
|
||||
|
||||
private sealed class Entry
|
||||
{
|
||||
public UIPanelKind kind;
|
||||
public GameObject root;
|
||||
public Action closeAction;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
public void Push(UIPanelKind kind, GameObject root, Action closeAction)
|
||||
{
|
||||
if (root == null) { return; }
|
||||
|
||||
Remove(root);
|
||||
|
||||
stack.Add(new Entry
|
||||
{
|
||||
kind = kind,
|
||||
root = root,
|
||||
closeAction = closeAction
|
||||
});
|
||||
if (debugLogs) Debug.Log($"[UIPanelStack] Push kind={kind} root={root.name} count={stack.Count}");
|
||||
}
|
||||
|
||||
public bool Remove(GameObject root)
|
||||
{
|
||||
if (root == null) { return false; }
|
||||
|
||||
for (int i = stack.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (stack[i].root == root)
|
||||
{
|
||||
stack.RemoveAt(i);
|
||||
if (debugLogs) Debug.Log($"[UIPanelStack] Remove root={root.name} count={stack.Count}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CloseTop()
|
||||
{
|
||||
CleanupInactive();
|
||||
|
||||
if (stack.Count == 0) { return false; }
|
||||
|
||||
Entry top = stack[stack.Count - 1];
|
||||
stack.RemoveAt(stack.Count - 1);
|
||||
|
||||
if (top.root == null) { return false; }
|
||||
|
||||
if (top.closeAction != null)
|
||||
{
|
||||
if (debugLogs) Debug.Log($"[UIPanelStack] CloseTop via action kind={top.kind} root={top.root.name} remain={stack.Count}");
|
||||
top.closeAction.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (debugLogs) Debug.Log($"[UIPanelStack] CloseTop via SetActive(false) kind={top.kind} root={top.root.name} remain={stack.Count}");
|
||||
top.root.SetActive(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetEscapeOpenHandler(Func<bool> handler)
|
||||
{
|
||||
escapeOpenHandler = handler;
|
||||
if (debugLogs) Debug.Log($"[UIPanelStack] SetEscapeOpenHandler set={(handler!=null)}");
|
||||
}
|
||||
|
||||
public void ClearEscapeOpenHandler(Func<bool> handler)
|
||||
{
|
||||
if (handler == null) { return; }
|
||||
if (escapeOpenHandler == handler)
|
||||
{
|
||||
escapeOpenHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryOpenOnEscapeWhenEmpty()
|
||||
{
|
||||
if (escapeOpenHandler == null) { return false; }
|
||||
bool result = escapeOpenHandler.Invoke();
|
||||
if (debugLogs) Debug.Log($"[UIPanelStack] TryOpenOnEscapeWhenEmpty result={result}");
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool IsAnyOpen()
|
||||
{
|
||||
CleanupInactive();
|
||||
return stack.Count > 0;
|
||||
}
|
||||
|
||||
private void CleanupInactive()
|
||||
{
|
||||
for (int i = stack.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (stack[i].root == null || !stack[i].root.activeInHierarchy)
|
||||
{
|
||||
stack.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35ee98817252d334e8278aafbe75db00
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,35 @@
|
||||
using UnityEngine;
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem;
|
||||
#endif
|
||||
|
||||
namespace UI.PanelStack
|
||||
{
|
||||
public class UIPanelStackInput : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private bool enabledInput = true;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!enabledInput) { return; }
|
||||
if (UIPanelStack.Instance == null) { return; }
|
||||
|
||||
if (IsEscapePressed())
|
||||
{
|
||||
if (!UIPanelStack.Instance.CloseTop())
|
||||
{
|
||||
UIPanelStack.Instance.TryOpenOnEscapeWhenEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsEscapePressed()
|
||||
{
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return Keyboard.current != null && Keyboard.current.escapeKey.wasPressedThisFrame;
|
||||
#else
|
||||
return Input.GetKeyDown(KeyCode.Escape);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf4367c679ee66743bd76b7243797601
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e16e6dab79f1324a8b20e0f1b380c1f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,154 @@
|
||||
using Core.InputLock;
|
||||
using Core.SaveSystem;
|
||||
using Michsky.UI.Reach;
|
||||
using UI.PanelStack;
|
||||
using UI.Settings;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace UI.PauseMenu
|
||||
{
|
||||
public class InGamePauseMenuController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private bool debugLogs = true;
|
||||
[Header("Root")]
|
||||
[SerializeField] private GameObject menuRoot;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField] private ButtonManager saveAndExitButton;
|
||||
[SerializeField] private ButtonManager settingsButton;
|
||||
[SerializeField] private ButtonManager returnToGameButton;
|
||||
|
||||
[Header("Settings Panel")]
|
||||
[SerializeField] private SettingsController settingsController;
|
||||
|
||||
[Header("Exit")]
|
||||
[SerializeField] private int titleSceneBuildIndex = 1;
|
||||
|
||||
private bool isWired;
|
||||
private bool handlerRegistered;
|
||||
private System.Func<bool> cachedEscapeHandler;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (menuRoot == null) { menuRoot = gameObject; }
|
||||
if (settingsController == null) { settingsController = FindObjectOfType<SettingsController>(true); }
|
||||
WireOnce();
|
||||
menuRoot.SetActive(false);
|
||||
|
||||
if (cachedEscapeHandler == null) { cachedEscapeHandler = TryOpenFromEscape; }
|
||||
if (debugLogs) Debug.Log($"[InGamePause] Awake. menuRoot={menuRoot.name} settingsController={(settingsController!=null)}");
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
StartCoroutine(RegisterEscapeHandlerWhenReady());
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (UIPanelStack.Instance != null)
|
||||
{
|
||||
UIPanelStack.Instance.ClearEscapeOpenHandler(cachedEscapeHandler);
|
||||
}
|
||||
handlerRegistered = false;
|
||||
}
|
||||
|
||||
private IEnumerator RegisterEscapeHandlerWhenReady()
|
||||
{
|
||||
while (enabled && gameObject.activeInHierarchy && UIPanelStack.Instance == null)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (!enabled || !gameObject.activeInHierarchy) { yield break; }
|
||||
if (UIPanelStack.Instance == null) { yield break; }
|
||||
if (handlerRegistered) { yield break; }
|
||||
|
||||
UIPanelStack.Instance.SetEscapeOpenHandler(cachedEscapeHandler);
|
||||
handlerRegistered = true;
|
||||
if (debugLogs) Debug.Log("[InGamePause] Escape handler registered.");
|
||||
}
|
||||
|
||||
private bool TryOpenFromEscape()
|
||||
{
|
||||
if (menuRoot == null) { return false; }
|
||||
if (menuRoot.activeInHierarchy) { return false; }
|
||||
if (debugLogs) Debug.Log("[InGamePause] TryOpenFromEscape -> Open()");
|
||||
Open();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Open()
|
||||
{
|
||||
if (menuRoot == null) { return; }
|
||||
if (menuRoot.activeInHierarchy) { return; }
|
||||
|
||||
menuRoot.SetActive(true);
|
||||
if (debugLogs) Debug.Log("[InGamePause] Open.");
|
||||
if (UIPanelStack.Instance != null)
|
||||
{
|
||||
UIPanelStack.Instance.Push(UIPanelKind.Pause, menuRoot, Close);
|
||||
}
|
||||
|
||||
if (PlayerControlLockService.Instance != null)
|
||||
{
|
||||
PlayerControlLockService.Instance.Lock(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (menuRoot == null) { return; }
|
||||
|
||||
if (UIPanelStack.Instance != null)
|
||||
{
|
||||
UIPanelStack.Instance.Remove(menuRoot);
|
||||
}
|
||||
|
||||
menuRoot.SetActive(false);
|
||||
if (debugLogs) Debug.Log("[InGamePause] Close.");
|
||||
|
||||
if (PlayerControlLockService.Instance != null)
|
||||
{
|
||||
PlayerControlLockService.Instance.Unlock(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void WireOnce()
|
||||
{
|
||||
if (isWired) { return; }
|
||||
|
||||
if (saveAndExitButton != null) { saveAndExitButton.onClick.AddListener(OnSaveAndExitClicked); }
|
||||
if (settingsButton != null) { settingsButton.onClick.AddListener(OnSettingsClicked); }
|
||||
if (returnToGameButton != null) { returnToGameButton.onClick.AddListener(Close); }
|
||||
|
||||
isWired = true;
|
||||
}
|
||||
|
||||
private void OnSettingsClicked()
|
||||
{
|
||||
if (debugLogs) Debug.Log($"[InGamePause] Settings clicked. settingsController={(settingsController!=null)}");
|
||||
if (settingsController != null)
|
||||
{
|
||||
settingsController.OpenAtIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSaveAndExitClicked()
|
||||
{
|
||||
if (SaveManager.Instance != null)
|
||||
{
|
||||
SaveManager.Instance.SaveGame();
|
||||
}
|
||||
|
||||
Close();
|
||||
|
||||
if (titleSceneBuildIndex >= 0 && titleSceneBuildIndex < SceneManager.sceneCountInBuildSettings)
|
||||
{
|
||||
SceneManager.LoadSceneAsync(titleSceneBuildIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e97f0f4ec4dd48b4caa77487e3726012
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,130 @@
|
||||
using UnityEngine;
|
||||
using Core.SceneLoading;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
using DG.Tweening;
|
||||
using Michsky.UI.Reach;
|
||||
|
||||
public class PressToContinue : MonoBehaviour
|
||||
{
|
||||
[SerializeField] FeedNotification notification;
|
||||
[SerializeField] CanvasGroup notificationCanvasGroup;
|
||||
[SerializeField, Range(0f, 1f)] float minAlpha = 0.25f;
|
||||
[SerializeField, Range(0f, 1f)] float maxAlpha = 1f;
|
||||
[SerializeField] float blinkDuration = 1.2f;
|
||||
[SerializeField] float blinkStartDelay = 1f;
|
||||
|
||||
Tween blinkTween;
|
||||
Tween blinkDelayTween;
|
||||
bool isShown;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (notification == null)
|
||||
{
|
||||
notification = FindObjectOfType<FeedNotification>(true);
|
||||
}
|
||||
|
||||
if (notificationCanvasGroup == null && notification != null)
|
||||
{
|
||||
notificationCanvasGroup = notification.GetComponent<CanvasGroup>();
|
||||
if (notificationCanvasGroup == null)
|
||||
{
|
||||
notificationCanvasGroup = notification.gameObject.AddComponent<CanvasGroup>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
isShown = false;
|
||||
|
||||
if (StartupSceneLoader.Instance != null)
|
||||
{
|
||||
StartupSceneLoader.Instance.BeginLoad();
|
||||
}
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
if (StartupSceneLoader.Instance == null) return;
|
||||
|
||||
if (!isShown && StartupSceneLoader.Instance.IsLoadComplete())
|
||||
{
|
||||
ShowNotification();
|
||||
}
|
||||
|
||||
if (!isShown) return;
|
||||
if (Keyboard.current == null) return;
|
||||
if (!Keyboard.current.anyKey.wasPressedThisFrame) return;
|
||||
|
||||
StartupSceneLoader.Instance.ActivateLoadedScene();
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
StopBlink();
|
||||
}
|
||||
|
||||
void ShowNotification()
|
||||
{
|
||||
if (notification == null) return;
|
||||
|
||||
notification.defaultState = FeedNotification.DefaultState.Expanded;
|
||||
notification.minimizeAfter = 0f;
|
||||
isShown = true;
|
||||
|
||||
if (notificationCanvasGroup != null)
|
||||
{
|
||||
notificationCanvasGroup.alpha = maxAlpha;
|
||||
}
|
||||
|
||||
StopBlink();
|
||||
notification.ExpandNotification();
|
||||
StartBlinkAfterDelay();
|
||||
}
|
||||
|
||||
void StartBlinkAfterDelay()
|
||||
{
|
||||
if (notificationCanvasGroup == null) return;
|
||||
|
||||
if (blinkDelayTween != null && blinkDelayTween.IsActive())
|
||||
{
|
||||
blinkDelayTween.Kill();
|
||||
}
|
||||
|
||||
blinkDelayTween = DOVirtual.DelayedCall(blinkStartDelay, StartBlink).SetUpdate(true);
|
||||
}
|
||||
|
||||
void StartBlink()
|
||||
{
|
||||
if (notificationCanvasGroup == null) return;
|
||||
|
||||
if (blinkTween != null && blinkTween.IsActive())
|
||||
{
|
||||
blinkTween.Kill();
|
||||
}
|
||||
|
||||
notificationCanvasGroup.alpha = maxAlpha;
|
||||
blinkTween = DOTween
|
||||
.To(() => notificationCanvasGroup.alpha, a => notificationCanvasGroup.alpha = a, minAlpha, blinkDuration)
|
||||
.SetEase(Ease.InOutSine)
|
||||
.SetLoops(-1, LoopType.Yoyo)
|
||||
.SetUpdate(true);
|
||||
}
|
||||
|
||||
void StopBlink()
|
||||
{
|
||||
if (blinkDelayTween != null && blinkDelayTween.IsActive())
|
||||
{
|
||||
blinkDelayTween.Kill();
|
||||
}
|
||||
blinkDelayTween = null;
|
||||
|
||||
if (blinkTween != null && blinkTween.IsActive())
|
||||
{
|
||||
blinkTween.Kill();
|
||||
}
|
||||
blinkTween = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20aa11c086716bb4b9abb7d8fc1d36f6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25d0873fc1a4f684aa4356c55166d02f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,754 @@
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using Core.SettingsSystem;
|
||||
using Michsky.UI.Reach;
|
||||
using UnityEngine;
|
||||
using UI.PanelStack;
|
||||
using Core.InputLock;
|
||||
|
||||
namespace UI.Settings
|
||||
{
|
||||
public class SettingsController : MonoBehaviour
|
||||
{
|
||||
private struct ResolutionOption
|
||||
{
|
||||
public int width;
|
||||
public int height;
|
||||
public int refreshRate;
|
||||
}
|
||||
|
||||
[SerializeField] private bool debugLogs = true;
|
||||
[Header("Root")]
|
||||
[SerializeField] private GameObject rootObject;
|
||||
[SerializeField] private PanelManager panelManager;
|
||||
|
||||
[Header("Hotkeys")]
|
||||
[SerializeField] private HotkeyEvent escapeHotkey;
|
||||
|
||||
[Header("Buttons (Optional)")]
|
||||
[SerializeField] private ButtonManager backButton;
|
||||
[SerializeField] private ButtonManager applyButton;
|
||||
[SerializeField] private ButtonManager resetButton;
|
||||
|
||||
[Header("Graphics - Dropdowns")]
|
||||
[SerializeField] private Dropdown resolutionDropdown;
|
||||
[SerializeField] private Dropdown displayModeDropdown;
|
||||
[SerializeField] private Dropdown qualityDropdown;
|
||||
[SerializeField] private Dropdown vSyncDropdown;
|
||||
[SerializeField] private Dropdown msaaDropdown;
|
||||
[SerializeField] private Dropdown textureQualityDropdown;
|
||||
|
||||
[Header("Graphics - Sliders")]
|
||||
[SerializeField] private SliderManager targetFpsSlider;
|
||||
[SerializeField] private SliderManager renderScaleSlider;
|
||||
[SerializeField] private SliderManager shadowDistanceSlider;
|
||||
|
||||
[Header("Audio - Sliders")]
|
||||
[SerializeField] private SliderManager masterVolumeSlider;
|
||||
[SerializeField] private SliderManager musicVolumeSlider;
|
||||
[SerializeField] private SliderManager sfxVolumeSlider;
|
||||
|
||||
[Header("Gameplay - Sliders")]
|
||||
[SerializeField] private SliderManager mouseSensitivitySlider;
|
||||
|
||||
[Header("Gameplay - Dropdowns")]
|
||||
[SerializeField] private Dropdown llmProviderDropdown;
|
||||
|
||||
[Header("Auto Initialize")]
|
||||
[SerializeField] private bool initResolutionDropdown = true;
|
||||
[SerializeField] private bool initQualityDropdown = true;
|
||||
[SerializeField] private bool delayRefreshOneFrameOnEnable = true;
|
||||
[SerializeField] private bool startHidden = true;
|
||||
|
||||
[Header("Dropdown Item Source")]
|
||||
[SerializeField] private bool rebuildResolutionItems = true;
|
||||
[SerializeField] private bool rebuildDisplayModeItems = true;
|
||||
[SerializeField] private bool rebuildQualityItems = true;
|
||||
[SerializeField] private bool rebuildVSyncItems = true;
|
||||
[SerializeField] private bool rebuildMsaaItems = true;
|
||||
[SerializeField] private bool rebuildTextureQualityItems = true;
|
||||
[SerializeField] private bool rebuildLlmProviderItems = true;
|
||||
|
||||
[Header("Ranges")]
|
||||
[SerializeField] private Vector2Int targetFpsRange = new Vector2Int(1, 240);
|
||||
[SerializeField] private Vector2 renderScalePercentRange = new Vector2(0.1f, 200f);
|
||||
[SerializeField] private Vector2 shadowDistanceRange = new Vector2(0f, 150f);
|
||||
[SerializeField] private Vector2 sensitivityRange = new Vector2(0.1f, 10f);
|
||||
|
||||
private SettingsData applied;
|
||||
private SettingsData working;
|
||||
private bool isDirty;
|
||||
private bool isWired;
|
||||
private bool isPopulating;
|
||||
private bool isClosing;
|
||||
private ResolutionOption[] resolutionOptions = System.Array.Empty<ResolutionOption>();
|
||||
|
||||
private float RenderScalePercentMin => Mathf.Max(renderScalePercentRange.x, 10f);
|
||||
private float RenderScalePercentMax => Mathf.Max(RenderScalePercentMin, renderScalePercentRange.y);
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
WireOnce();
|
||||
if (startHidden)
|
||||
{
|
||||
ShowRoot(false);
|
||||
}
|
||||
if (debugLogs) Debug.Log($"[SettingsController] Awake. startHidden={startHidden} rootActive={(rootObject!=null?rootObject.activeSelf:gameObject.activeSelf)}");
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
WireOnce();
|
||||
ShowRoot(true);
|
||||
if (delayRefreshOneFrameOnEnable) { StartCoroutine(RefreshNextFrame()); }
|
||||
else { RefreshFromApplied(); }
|
||||
}
|
||||
|
||||
private IEnumerator RefreshNextFrame()
|
||||
{
|
||||
yield return null;
|
||||
RefreshFromApplied();
|
||||
}
|
||||
|
||||
public void Show()
|
||||
{
|
||||
RefreshFromApplied();
|
||||
ShowRoot(true);
|
||||
if (panelManager != null) { panelManager.ShowCurrentPanel(); }
|
||||
}
|
||||
|
||||
public void SetResolutionIndex(int index) { OnResolutionChanged(index); }
|
||||
public void SetDisplayModeIndex(int index) { OnDisplayModeChanged(index); }
|
||||
public void SetQualityIndex(int index) { OnQualityChanged(index); }
|
||||
public void SetVSyncIndex(int index) { OnVSyncChanged(index); }
|
||||
public void SetMsaaIndex(int index) { OnMsaaChanged(index); }
|
||||
public void SetTextureQualityIndex(int index) { OnTextureQualityChanged(index); }
|
||||
public void SetTargetFps(float value) { OnTargetFpsChanged(value); }
|
||||
public void SetRenderScalePercent(float value) { OnRenderScalePercentChanged(value); }
|
||||
public void SetShadowDistance(float value) { OnShadowDistanceChanged(value); }
|
||||
public void SetMasterVolume(float value) { OnMasterChanged(value); }
|
||||
public void SetMusicVolume(float value) { OnMusicChanged(value); }
|
||||
public void SetSfxVolume(float value) { OnSfxChanged(value); }
|
||||
public void SetMouseSensitivity(float value) { OnSensitivityChanged(value); }
|
||||
public void SetLLMProviderIndex(int index) { OnLLMProviderChanged(index); }
|
||||
|
||||
public void CloseOrBack()
|
||||
{
|
||||
if (isClosing) { return; }
|
||||
isClosing = true;
|
||||
if (debugLogs) Debug.Log($"[SettingsController] CloseOrBack called. panelManagerActive={(panelManager!=null && panelManager.gameObject.activeInHierarchy)}");
|
||||
|
||||
if (isDirty)
|
||||
{
|
||||
working = Clone(applied);
|
||||
PopulateUI(working);
|
||||
isDirty = false;
|
||||
}
|
||||
|
||||
if (UIPanelStack.Instance != null)
|
||||
{
|
||||
UIPanelStack.Instance.Remove(rootObject != null ? rootObject : gameObject);
|
||||
}
|
||||
|
||||
if (PlayerControlLockService.Instance != null)
|
||||
{
|
||||
PlayerControlLockService.Instance.Unlock(this);
|
||||
}
|
||||
|
||||
float delay = 0f;
|
||||
if (panelManager != null && panelManager.isActiveAndEnabled && panelManager.gameObject.activeInHierarchy)
|
||||
{
|
||||
panelManager.HideCurrentPanel();
|
||||
delay = panelManager.cachedStateLength * panelManager.animationSpeed;
|
||||
}
|
||||
|
||||
if (rootObject != null) { StartCoroutine(HideAfter(delay)); }
|
||||
else { StartCoroutine(HideAfter(delay)); }
|
||||
}
|
||||
|
||||
public void OpenAtIndex(int index)
|
||||
{
|
||||
isClosing = false;
|
||||
if (debugLogs) Debug.Log($"[SettingsController] OpenAtIndex({index}) called. panelManager={(panelManager!=null)}");
|
||||
ShowRoot(true);
|
||||
if (panelManager != null && !panelManager.gameObject.activeInHierarchy)
|
||||
{
|
||||
panelManager.gameObject.SetActive(true);
|
||||
}
|
||||
if (panelManager != null) { panelManager.OpenPanelByIndex(index); }
|
||||
if (delayRefreshOneFrameOnEnable) { StartCoroutine(RefreshNextFrame()); }
|
||||
else { RefreshFromApplied(); }
|
||||
if (UIPanelStack.Instance != null) { UIPanelStack.Instance.Push(UIPanelKind.Settings, rootObject != null ? rootObject : gameObject, CloseOrBack); }
|
||||
|
||||
if (PlayerControlLockService.Instance != null)
|
||||
{
|
||||
PlayerControlLockService.Instance.Lock(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void Apply()
|
||||
{
|
||||
SettingsService service = SettingsService.Instance;
|
||||
if (service == null) { return; }
|
||||
if (debugLogs) Debug.Log($"[SettingsController] Apply clicked. gameplay.mouseSensitivity={working?.gameplay?.mouseSensitivity}");
|
||||
service.ApplyAndSave(Clone(working));
|
||||
applied = Clone(service.Current);
|
||||
isDirty = false;
|
||||
}
|
||||
|
||||
public void ResetToDefaults()
|
||||
{
|
||||
working = SettingsData.Default();
|
||||
PopulateUI(working);
|
||||
isDirty = true;
|
||||
}
|
||||
|
||||
private IEnumerator HideAfter(float seconds)
|
||||
{
|
||||
if (seconds > 0f) { yield return new WaitForSecondsRealtime(seconds); }
|
||||
ShowRoot(false);
|
||||
isClosing = false;
|
||||
}
|
||||
|
||||
private void ShowRoot(bool show)
|
||||
{
|
||||
if (rootObject != null) { rootObject.SetActive(show); }
|
||||
else { gameObject.SetActive(show); }
|
||||
if (debugLogs) Debug.Log($"[SettingsController] ShowRoot({show}).");
|
||||
}
|
||||
|
||||
private void RefreshFromApplied()
|
||||
{
|
||||
SettingsService service = SettingsService.Instance;
|
||||
applied = service != null ? Clone(service.Current) : SettingsData.Default();
|
||||
working = Clone(applied);
|
||||
EnsureDropdownItems();
|
||||
ApplyRanges();
|
||||
PopulateUI(working);
|
||||
isDirty = false;
|
||||
}
|
||||
|
||||
private void WireOnce()
|
||||
{
|
||||
if (isWired) { return; }
|
||||
|
||||
if (escapeHotkey != null) { escapeHotkey.onHotkeyPress.AddListener(CloseOrBack); }
|
||||
if (backButton != null) { backButton.onClick.AddListener(CloseOrBack); }
|
||||
if (applyButton != null) { applyButton.onClick.AddListener(Apply); }
|
||||
if (resetButton != null) { resetButton.onClick.AddListener(ResetToDefaults); }
|
||||
|
||||
if (resolutionDropdown != null) { resolutionDropdown.onValueChanged.AddListener(OnResolutionChanged); }
|
||||
if (displayModeDropdown != null) { displayModeDropdown.onValueChanged.AddListener(OnDisplayModeChanged); }
|
||||
if (qualityDropdown != null) { qualityDropdown.onValueChanged.AddListener(OnQualityChanged); }
|
||||
if (vSyncDropdown != null) { vSyncDropdown.onValueChanged.AddListener(OnVSyncChanged); }
|
||||
if (msaaDropdown != null) { msaaDropdown.onValueChanged.AddListener(OnMsaaChanged); }
|
||||
if (textureQualityDropdown != null) { textureQualityDropdown.onValueChanged.AddListener(OnTextureQualityChanged); }
|
||||
|
||||
if (targetFpsSlider != null) { targetFpsSlider.onValueChanged.AddListener(OnTargetFpsChanged); }
|
||||
if (renderScaleSlider != null) { renderScaleSlider.onValueChanged.AddListener(OnRenderScalePercentChanged); }
|
||||
if (shadowDistanceSlider != null) { shadowDistanceSlider.onValueChanged.AddListener(OnShadowDistanceChanged); }
|
||||
|
||||
if (masterVolumeSlider != null) { masterVolumeSlider.onValueChanged.AddListener(OnMasterChanged); }
|
||||
if (musicVolumeSlider != null) { musicVolumeSlider.onValueChanged.AddListener(OnMusicChanged); }
|
||||
if (sfxVolumeSlider != null) { sfxVolumeSlider.onValueChanged.AddListener(OnSfxChanged); }
|
||||
|
||||
if (mouseSensitivitySlider != null) { mouseSensitivitySlider.onValueChanged.AddListener(OnSensitivityChanged); }
|
||||
if (llmProviderDropdown != null) { llmProviderDropdown.onValueChanged.AddListener(OnLLMProviderChanged); }
|
||||
|
||||
isWired = true;
|
||||
}
|
||||
|
||||
private void EnsureDropdownItems()
|
||||
{
|
||||
if (initResolutionDropdown && resolutionDropdown != null && (rebuildResolutionItems || resolutionDropdown.items == null || resolutionDropdown.items.Count == 0))
|
||||
{
|
||||
resolutionDropdown.items.Clear();
|
||||
var resolutions = Screen.resolutions;
|
||||
int currentWidth = Screen.width;
|
||||
int currentHeight = Screen.height;
|
||||
#if UNITY_2022_2_OR_NEWER
|
||||
int currentRefresh = Mathf.RoundToInt((float)Screen.currentResolution.refreshRateRatio.value);
|
||||
#else
|
||||
int currentRefresh = Screen.currentResolution.refreshRate;
|
||||
#endif
|
||||
var grouped = resolutions
|
||||
.GroupBy(r => new Vector2Int(r.width, r.height))
|
||||
.Select(g =>
|
||||
{
|
||||
var list = g.ToArray();
|
||||
int bestIndex = 0;
|
||||
int bestRefresh = -1;
|
||||
for (int i = 0; i < list.Length; i++)
|
||||
{
|
||||
#if UNITY_2022_2_OR_NEWER
|
||||
int rr = Mathf.RoundToInt((float)list[i].refreshRateRatio.value);
|
||||
#else
|
||||
int rr = list[i].refreshRate;
|
||||
#endif
|
||||
bool prefer = rr == currentRefresh;
|
||||
if (prefer)
|
||||
{
|
||||
bestIndex = i;
|
||||
bestRefresh = rr;
|
||||
break;
|
||||
}
|
||||
if (rr > bestRefresh)
|
||||
{
|
||||
bestIndex = i;
|
||||
bestRefresh = rr;
|
||||
}
|
||||
}
|
||||
var chosen = list[bestIndex];
|
||||
return new ResolutionOption
|
||||
{
|
||||
width = chosen.width,
|
||||
height = chosen.height,
|
||||
refreshRate = Mathf.Max(0, bestRefresh)
|
||||
};
|
||||
})
|
||||
.OrderBy(o => o.width)
|
||||
.ThenBy(o => o.height)
|
||||
.ToArray();
|
||||
|
||||
resolutionOptions = grouped;
|
||||
int currentIndex = 0;
|
||||
for (int i = 0; i < resolutionOptions.Length; i++)
|
||||
{
|
||||
var opt = resolutionOptions[i];
|
||||
string label = opt.refreshRate > 0 ? $"{opt.width}x{opt.height} @ {opt.refreshRate}Hz" : $"{opt.width}x{opt.height}";
|
||||
resolutionDropdown.CreateNewItem(label, false);
|
||||
if (opt.width == currentWidth && opt.height == currentHeight) { currentIndex = i; }
|
||||
}
|
||||
resolutionDropdown.selectedItemIndex = Mathf.Clamp(currentIndex, 0, Mathf.Max(0, resolutionDropdown.items.Count - 1));
|
||||
resolutionDropdown.Initialize();
|
||||
}
|
||||
else if (resolutionDropdown != null)
|
||||
{
|
||||
EnsureDropdownInitialized(resolutionDropdown);
|
||||
}
|
||||
|
||||
if (displayModeDropdown != null && (rebuildDisplayModeItems || displayModeDropdown.items == null || displayModeDropdown.items.Count == 0))
|
||||
{
|
||||
displayModeDropdown.items.Clear();
|
||||
displayModeDropdown.CreateNewItem("Fullscreen", false);
|
||||
displayModeDropdown.CreateNewItem("Borderless", false);
|
||||
displayModeDropdown.CreateNewItem("Windowed", false);
|
||||
displayModeDropdown.Initialize();
|
||||
}
|
||||
else if (displayModeDropdown != null)
|
||||
{
|
||||
EnsureDropdownInitialized(displayModeDropdown);
|
||||
}
|
||||
|
||||
if (initQualityDropdown && qualityDropdown != null && (rebuildQualityItems || qualityDropdown.items == null || qualityDropdown.items.Count == 0))
|
||||
{
|
||||
qualityDropdown.items.Clear();
|
||||
foreach (string q in QualitySettings.names) { qualityDropdown.CreateNewItem(q, false); }
|
||||
qualityDropdown.Initialize();
|
||||
}
|
||||
else if (qualityDropdown != null)
|
||||
{
|
||||
EnsureDropdownInitialized(qualityDropdown);
|
||||
}
|
||||
|
||||
if (vSyncDropdown != null && (rebuildVSyncItems || vSyncDropdown.items == null || vSyncDropdown.items.Count == 0))
|
||||
{
|
||||
vSyncDropdown.items.Clear();
|
||||
vSyncDropdown.CreateNewItem("Off", false);
|
||||
vSyncDropdown.CreateNewItem("On", false);
|
||||
vSyncDropdown.Initialize();
|
||||
}
|
||||
else if (vSyncDropdown != null)
|
||||
{
|
||||
EnsureDropdownInitialized(vSyncDropdown);
|
||||
}
|
||||
|
||||
if (msaaDropdown != null && (rebuildMsaaItems || msaaDropdown.items == null || msaaDropdown.items.Count == 0))
|
||||
{
|
||||
msaaDropdown.items.Clear();
|
||||
msaaDropdown.CreateNewItem("Off", false);
|
||||
msaaDropdown.CreateNewItem("2x", false);
|
||||
msaaDropdown.CreateNewItem("4x", false);
|
||||
msaaDropdown.CreateNewItem("8x", false);
|
||||
msaaDropdown.Initialize();
|
||||
}
|
||||
else if (msaaDropdown != null)
|
||||
{
|
||||
EnsureDropdownInitialized(msaaDropdown);
|
||||
}
|
||||
|
||||
if (textureQualityDropdown != null && (rebuildTextureQualityItems || textureQualityDropdown.items == null || textureQualityDropdown.items.Count == 0))
|
||||
{
|
||||
textureQualityDropdown.items.Clear();
|
||||
textureQualityDropdown.CreateNewItem("Full", false);
|
||||
textureQualityDropdown.CreateNewItem("Half", false);
|
||||
textureQualityDropdown.CreateNewItem("Quarter", false);
|
||||
textureQualityDropdown.CreateNewItem("Eighth", false);
|
||||
textureQualityDropdown.Initialize();
|
||||
}
|
||||
else if (textureQualityDropdown != null)
|
||||
{
|
||||
EnsureDropdownInitialized(textureQualityDropdown);
|
||||
}
|
||||
|
||||
if (llmProviderDropdown != null && (rebuildLlmProviderItems || llmProviderDropdown.items == null || llmProviderDropdown.items.Count == 0))
|
||||
{
|
||||
llmProviderDropdown.items.Clear();
|
||||
llmProviderDropdown.CreateNewItem("DeepSeek", false);
|
||||
llmProviderDropdown.CreateNewItem("Doubao", false);
|
||||
llmProviderDropdown.Initialize();
|
||||
}
|
||||
else if (llmProviderDropdown != null)
|
||||
{
|
||||
EnsureDropdownInitialized(llmProviderDropdown);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureDropdownInitialized(Dropdown dropdown)
|
||||
{
|
||||
if (dropdown.items == null || dropdown.items.Count == 0) { return; }
|
||||
for (int i = 0; i < dropdown.items.Count; i++)
|
||||
{
|
||||
if (dropdown.items[i].itemButton == null)
|
||||
{
|
||||
dropdown.Initialize();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyRanges()
|
||||
{
|
||||
if (targetFpsSlider != null && targetFpsSlider.mainSlider != null)
|
||||
{
|
||||
targetFpsSlider.mainSlider.minValue = targetFpsRange.x;
|
||||
targetFpsSlider.mainSlider.maxValue = targetFpsRange.y;
|
||||
targetFpsSlider.useRoundValue = true;
|
||||
}
|
||||
|
||||
if (renderScaleSlider != null && renderScaleSlider.mainSlider != null)
|
||||
{
|
||||
renderScaleSlider.mainSlider.minValue = RenderScalePercentMin;
|
||||
renderScaleSlider.mainSlider.maxValue = RenderScalePercentMax;
|
||||
renderScaleSlider.usePercent = true;
|
||||
renderScaleSlider.useRoundValue = true;
|
||||
}
|
||||
|
||||
if (shadowDistanceSlider != null && shadowDistanceSlider.mainSlider != null)
|
||||
{
|
||||
shadowDistanceSlider.mainSlider.minValue = shadowDistanceRange.x;
|
||||
shadowDistanceSlider.mainSlider.maxValue = shadowDistanceRange.y;
|
||||
shadowDistanceSlider.useRoundValue = false;
|
||||
}
|
||||
|
||||
if (masterVolumeSlider != null && masterVolumeSlider.mainSlider != null)
|
||||
{
|
||||
masterVolumeSlider.mainSlider.minValue = 0f;
|
||||
masterVolumeSlider.mainSlider.maxValue = 1f;
|
||||
masterVolumeSlider.usePercent = true;
|
||||
}
|
||||
|
||||
if (musicVolumeSlider != null && musicVolumeSlider.mainSlider != null)
|
||||
{
|
||||
musicVolumeSlider.mainSlider.minValue = 0f;
|
||||
musicVolumeSlider.mainSlider.maxValue = 1f;
|
||||
musicVolumeSlider.usePercent = true;
|
||||
}
|
||||
|
||||
if (sfxVolumeSlider != null && sfxVolumeSlider.mainSlider != null)
|
||||
{
|
||||
sfxVolumeSlider.mainSlider.minValue = 0f;
|
||||
sfxVolumeSlider.mainSlider.maxValue = 1f;
|
||||
sfxVolumeSlider.usePercent = true;
|
||||
}
|
||||
|
||||
if (mouseSensitivitySlider != null && mouseSensitivitySlider.mainSlider != null)
|
||||
{
|
||||
mouseSensitivitySlider.mainSlider.minValue = sensitivityRange.x;
|
||||
mouseSensitivitySlider.mainSlider.maxValue = sensitivityRange.y;
|
||||
mouseSensitivitySlider.useRoundValue = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateUI(SettingsData data)
|
||||
{
|
||||
if (data == null) { return; }
|
||||
isPopulating = true;
|
||||
|
||||
if (resolutionDropdown != null)
|
||||
{
|
||||
int idx = -1;
|
||||
if (data.graphics.resolutionWidth > 0 && data.graphics.resolutionHeight > 0 && resolutionOptions.Length > 0)
|
||||
{
|
||||
int best = -1;
|
||||
int bestRefreshDiff = int.MaxValue;
|
||||
for (int i = 0; i < resolutionOptions.Length; i++)
|
||||
{
|
||||
var opt = resolutionOptions[i];
|
||||
if (opt.width != data.graphics.resolutionWidth || opt.height != data.graphics.resolutionHeight) { continue; }
|
||||
if (data.graphics.resolutionRefreshRate > 0 && opt.refreshRate > 0)
|
||||
{
|
||||
int diff = Mathf.Abs(opt.refreshRate - data.graphics.resolutionRefreshRate);
|
||||
if (diff < bestRefreshDiff) { bestRefreshDiff = diff; best = i; }
|
||||
}
|
||||
else
|
||||
{
|
||||
best = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
idx = best;
|
||||
}
|
||||
else if (data.graphics.resolutionIndex >= 0)
|
||||
{
|
||||
var resolutions = Screen.resolutions;
|
||||
if (data.graphics.resolutionIndex >= 0 && data.graphics.resolutionIndex < resolutions.Length && resolutionOptions.Length > 0)
|
||||
{
|
||||
var r = resolutions[data.graphics.resolutionIndex];
|
||||
for (int i = 0; i < resolutionOptions.Length; i++)
|
||||
{
|
||||
if (resolutionOptions[i].width == r.width && resolutionOptions[i].height == r.height) { idx = i; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (idx < 0) { idx = resolutionDropdown.selectedItemIndex; }
|
||||
idx = Mathf.Clamp(idx, 0, Mathf.Max(0, resolutionDropdown.items.Count - 1));
|
||||
EnsureDropdownInitialized(resolutionDropdown);
|
||||
if (resolutionDropdown.items != null && resolutionDropdown.items.Count > 0) { resolutionDropdown.SetDropdownIndex(idx); }
|
||||
}
|
||||
|
||||
if (displayModeDropdown != null)
|
||||
{
|
||||
int modeIndex = FullscreenModeToIndex(data.graphics.fullscreenMode);
|
||||
EnsureDropdownInitialized(displayModeDropdown);
|
||||
if (displayModeDropdown.items != null && displayModeDropdown.items.Count > 0) { displayModeDropdown.SetDropdownIndex(modeIndex); }
|
||||
}
|
||||
|
||||
if (qualityDropdown != null)
|
||||
{
|
||||
int q = data.graphics.qualityLevel;
|
||||
if (q < 0) { q = QualitySettings.GetQualityLevel(); }
|
||||
q = Mathf.Clamp(q, 0, Mathf.Max(0, qualityDropdown.items.Count - 1));
|
||||
EnsureDropdownInitialized(qualityDropdown);
|
||||
if (qualityDropdown.items != null && qualityDropdown.items.Count > 0) { qualityDropdown.SetDropdownIndex(q); }
|
||||
}
|
||||
|
||||
if (vSyncDropdown != null)
|
||||
{
|
||||
EnsureDropdownInitialized(vSyncDropdown);
|
||||
if (vSyncDropdown.items != null && vSyncDropdown.items.Count > 0) { vSyncDropdown.SetDropdownIndex(data.graphics.vSync ? 1 : 0); }
|
||||
}
|
||||
|
||||
if (msaaDropdown != null)
|
||||
{
|
||||
EnsureDropdownInitialized(msaaDropdown);
|
||||
if (msaaDropdown.items != null && msaaDropdown.items.Count > 0) { msaaDropdown.SetDropdownIndex(MsaaToIndex(data.graphics.msaaSampleCount)); }
|
||||
}
|
||||
|
||||
if (textureQualityDropdown != null)
|
||||
{
|
||||
int t = Mathf.Clamp(data.graphics.textureMipmapLimit, 0, 3);
|
||||
EnsureDropdownInitialized(textureQualityDropdown);
|
||||
if (textureQualityDropdown.items != null && textureQualityDropdown.items.Count > 0) { textureQualityDropdown.SetDropdownIndex(t); }
|
||||
}
|
||||
|
||||
if (targetFpsSlider != null && targetFpsSlider.mainSlider != null)
|
||||
{
|
||||
float v = data.graphics.targetFps <= 0 ? 60 : data.graphics.targetFps;
|
||||
targetFpsSlider.mainSlider.value = v;
|
||||
}
|
||||
|
||||
if (renderScaleSlider != null && renderScaleSlider.mainSlider != null)
|
||||
{
|
||||
float percent = Mathf.Clamp(data.graphics.renderScale * 100f, RenderScalePercentMin, RenderScalePercentMax);
|
||||
renderScaleSlider.mainSlider.value = percent;
|
||||
}
|
||||
|
||||
if (shadowDistanceSlider != null && shadowDistanceSlider.mainSlider != null)
|
||||
{
|
||||
shadowDistanceSlider.mainSlider.value = Mathf.Clamp(data.graphics.shadowDistance, shadowDistanceRange.x, shadowDistanceRange.y);
|
||||
}
|
||||
|
||||
if (masterVolumeSlider != null && masterVolumeSlider.mainSlider != null)
|
||||
{
|
||||
masterVolumeSlider.mainSlider.value = Mathf.Clamp01(data.audio.master);
|
||||
}
|
||||
|
||||
if (musicVolumeSlider != null && musicVolumeSlider.mainSlider != null)
|
||||
{
|
||||
musicVolumeSlider.mainSlider.value = Mathf.Clamp01(data.audio.music);
|
||||
}
|
||||
|
||||
if (sfxVolumeSlider != null && sfxVolumeSlider.mainSlider != null)
|
||||
{
|
||||
sfxVolumeSlider.mainSlider.value = Mathf.Clamp01(data.audio.sfx);
|
||||
}
|
||||
|
||||
if (mouseSensitivitySlider != null && mouseSensitivitySlider.mainSlider != null)
|
||||
{
|
||||
mouseSensitivitySlider.mainSlider.value = Mathf.Clamp(data.gameplay.mouseSensitivity, sensitivityRange.x, sensitivityRange.y);
|
||||
}
|
||||
|
||||
if (llmProviderDropdown != null)
|
||||
{
|
||||
int p = Mathf.Clamp(data.gameplay.llmProvider, 0, 1);
|
||||
EnsureDropdownInitialized(llmProviderDropdown);
|
||||
if (llmProviderDropdown.items != null && llmProviderDropdown.items.Count > 0) { llmProviderDropdown.SetDropdownIndex(p); }
|
||||
}
|
||||
|
||||
isPopulating = false;
|
||||
}
|
||||
|
||||
private int FullscreenModeToIndex(int fullscreenMode)
|
||||
{
|
||||
if (fullscreenMode == (int)FullScreenMode.ExclusiveFullScreen) { return 0; }
|
||||
if (fullscreenMode == (int)FullScreenMode.FullScreenWindow) { return 1; }
|
||||
return 2;
|
||||
}
|
||||
|
||||
private int IndexToFullscreenMode(int index)
|
||||
{
|
||||
if (index == 0) { return (int)FullScreenMode.ExclusiveFullScreen; }
|
||||
if (index == 1) { return (int)FullScreenMode.FullScreenWindow; }
|
||||
return (int)FullScreenMode.Windowed;
|
||||
}
|
||||
|
||||
private int MsaaToIndex(int msaa)
|
||||
{
|
||||
if (msaa >= 8) { return 3; }
|
||||
if (msaa >= 4) { return 2; }
|
||||
if (msaa >= 2) { return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int IndexToMsaa(int index)
|
||||
{
|
||||
if (index == 3) { return 8; }
|
||||
if (index == 2) { return 4; }
|
||||
if (index == 1) { return 2; }
|
||||
return 1;
|
||||
}
|
||||
|
||||
private void MarkDirty()
|
||||
{
|
||||
if (isPopulating) { return; }
|
||||
isDirty = true;
|
||||
}
|
||||
|
||||
private void OnResolutionChanged(int index)
|
||||
{
|
||||
if (working == null) { return; }
|
||||
working.graphics.resolutionIndex = -1;
|
||||
if (resolutionOptions != null && index >= 0 && index < resolutionOptions.Length)
|
||||
{
|
||||
var opt = resolutionOptions[index];
|
||||
working.graphics.resolutionWidth = opt.width;
|
||||
working.graphics.resolutionHeight = opt.height;
|
||||
working.graphics.resolutionRefreshRate = opt.refreshRate;
|
||||
}
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnDisplayModeChanged(int index)
|
||||
{
|
||||
if (working == null) { return; }
|
||||
working.graphics.fullscreenMode = IndexToFullscreenMode(index);
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnQualityChanged(int index)
|
||||
{
|
||||
if (working == null) { return; }
|
||||
working.graphics.qualityLevel = index;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnVSyncChanged(int index)
|
||||
{
|
||||
if (working == null) { return; }
|
||||
working.graphics.vSync = index == 1;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnMsaaChanged(int index)
|
||||
{
|
||||
if (working == null) { return; }
|
||||
working.graphics.msaaSampleCount = IndexToMsaa(index);
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnLLMProviderChanged(int index)
|
||||
{
|
||||
if (working == null) { return; }
|
||||
working.gameplay.llmProvider = Mathf.Clamp(index, 0, 1);
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnTextureQualityChanged(int index)
|
||||
{
|
||||
if (working == null) { return; }
|
||||
working.graphics.textureMipmapLimit = Mathf.Clamp(index, 0, 3);
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnTargetFpsChanged(float value)
|
||||
{
|
||||
if (working == null) { return; }
|
||||
int fps = Mathf.RoundToInt(value);
|
||||
working.graphics.targetFps = fps;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnRenderScalePercentChanged(float value)
|
||||
{
|
||||
if (working == null) { return; }
|
||||
working.graphics.renderScale = Mathf.Clamp(value, RenderScalePercentMin, RenderScalePercentMax) / 100f;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnShadowDistanceChanged(float value)
|
||||
{
|
||||
if (working == null) { return; }
|
||||
working.graphics.shadowDistance = value;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnMasterChanged(float value)
|
||||
{
|
||||
if (working == null) { return; }
|
||||
working.audio.master = value;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnMusicChanged(float value)
|
||||
{
|
||||
if (working == null) { return; }
|
||||
working.audio.music = value;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnSfxChanged(float value)
|
||||
{
|
||||
if (working == null) { return; }
|
||||
working.audio.sfx = value;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void OnSensitivityChanged(float value)
|
||||
{
|
||||
if (working == null) { return; }
|
||||
working.gameplay.mouseSensitivity = value;
|
||||
if (debugLogs) Debug.Log($"[SettingsController] Sensitivity changed -> {value}");
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private SettingsData Clone(SettingsData data)
|
||||
{
|
||||
if (data == null) { return SettingsData.Default(); }
|
||||
string json = JsonUtility.ToJson(data);
|
||||
SettingsData copy = JsonUtility.FromJson<SettingsData>(json);
|
||||
return copy ?? SettingsData.Default();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab4fca7b993d48b4d97efb531d7ef681
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,232 @@
|
||||
using DG.Tweening;
|
||||
using TMPro;
|
||||
using Core.InputLock;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UI.PanelStack;
|
||||
using Michsky.UI.Reach;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class SpecialUsePopupController : MonoBehaviour
|
||||
{
|
||||
public static SpecialUsePopupController Instance { get; private set; }
|
||||
|
||||
[Header("UI References")]
|
||||
[SerializeField] private RectTransform popupImageRect;
|
||||
[SerializeField] private Image popupImage;
|
||||
[SerializeField] private TMP_Text popupText;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField] private ButtonManager closeButton;
|
||||
[SerializeField] private ButtonManager previousPageButton;
|
||||
[SerializeField] private ButtonManager nextPageButton;
|
||||
|
||||
[Header("Animation")]
|
||||
[SerializeField] private float hiddenY = -600f;
|
||||
[SerializeField] private float shownY = 0f;
|
||||
[SerializeField] private float tweenDuration = 0.35f;
|
||||
[SerializeField] private Ease tweenEase = Ease.OutCubic;
|
||||
|
||||
[Header("Control")]
|
||||
[SerializeField] private bool lockPlayerControl = true;
|
||||
[SerializeField] private bool usePanelStackEscapeClose = true;
|
||||
|
||||
private Sequence sequence;
|
||||
private string[] pages;
|
||||
private int pageIndex;
|
||||
private bool isOpen;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null) Instance = this;
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (popupImageRect == null)
|
||||
{
|
||||
popupImageRect = GetComponentInChildren<Image>(true)?.rectTransform;
|
||||
}
|
||||
|
||||
if (popupImage == null)
|
||||
{
|
||||
popupImage = popupImageRect != null ? popupImageRect.GetComponent<Image>() : GetComponentInChildren<Image>(true);
|
||||
}
|
||||
|
||||
if (popupText == null)
|
||||
{
|
||||
popupText = GetComponentInChildren<TMP_Text>(true);
|
||||
}
|
||||
|
||||
if (closeButton != null)
|
||||
{
|
||||
closeButton.onClick.RemoveListener(Close);
|
||||
closeButton.onClick.AddListener(Close);
|
||||
}
|
||||
|
||||
if (previousPageButton != null)
|
||||
{
|
||||
previousPageButton.onClick.RemoveListener(PreviousPage);
|
||||
previousPageButton.onClick.AddListener(PreviousPage);
|
||||
}
|
||||
|
||||
if (nextPageButton != null)
|
||||
{
|
||||
nextPageButton.onClick.RemoveListener(NextPage);
|
||||
nextPageButton.onClick.AddListener(NextPage);
|
||||
}
|
||||
|
||||
Hide(true);
|
||||
}
|
||||
|
||||
public void ShowPages(string[] pageTexts, Sprite sprite = null, int startPageIndex = 0)
|
||||
{
|
||||
if (popupImageRect == null) return;
|
||||
|
||||
pages = pageTexts != null && pageTexts.Length > 0 ? pageTexts : new[] { string.Empty };
|
||||
pageIndex = Mathf.Clamp(startPageIndex, 0, pages.Length - 1);
|
||||
SetPageText();
|
||||
|
||||
if (sprite != null && popupImage != null)
|
||||
{
|
||||
popupImage.sprite = sprite;
|
||||
}
|
||||
|
||||
Open();
|
||||
}
|
||||
|
||||
private void Open()
|
||||
{
|
||||
if (isOpen) return;
|
||||
isOpen = true;
|
||||
|
||||
gameObject.SetActive(true);
|
||||
popupImageRect.gameObject.SetActive(true);
|
||||
|
||||
if (usePanelStackEscapeClose && UIPanelStack.Instance != null)
|
||||
{
|
||||
UIPanelStack.Instance.Push(UIPanelKind.Custom, gameObject, CloseFromPanelStack);
|
||||
}
|
||||
|
||||
if (lockPlayerControl && PlayerControlLockService.Instance != null)
|
||||
{
|
||||
PlayerControlLockService.Instance.Lock(this);
|
||||
}
|
||||
|
||||
popupImageRect.anchoredPosition = new Vector2(popupImageRect.anchoredPosition.x, hiddenY);
|
||||
|
||||
sequence?.Kill();
|
||||
sequence = DOTween.Sequence();
|
||||
sequence.Append(popupImageRect.DOAnchorPosY(shownY, tweenDuration).SetEase(tweenEase));
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (!isOpen) return;
|
||||
if (UIPanelStack.Instance != null) UIPanelStack.Instance.Remove(gameObject);
|
||||
Hide(false);
|
||||
}
|
||||
|
||||
private void CloseFromPanelStack()
|
||||
{
|
||||
if (!isOpen) return;
|
||||
Hide(false);
|
||||
}
|
||||
|
||||
private void PreviousPage()
|
||||
{
|
||||
if (!isOpen) return;
|
||||
if (pages == null || pages.Length == 0) return;
|
||||
if (pageIndex <= 0) return;
|
||||
pageIndex--;
|
||||
SetPageText();
|
||||
}
|
||||
|
||||
private void NextPage()
|
||||
{
|
||||
if (!isOpen) return;
|
||||
if (pages == null || pages.Length == 0) return;
|
||||
if (pageIndex >= pages.Length - 1) return;
|
||||
pageIndex++;
|
||||
SetPageText();
|
||||
}
|
||||
|
||||
private void SetPageText()
|
||||
{
|
||||
if (popupText != null)
|
||||
{
|
||||
if (pages != null && pages.Length > 0)
|
||||
{
|
||||
int safeIndex = Mathf.Clamp(pageIndex, 0, pages.Length - 1);
|
||||
popupText.text = pages[safeIndex] ?? string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
popupText.text = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
bool hasPages = pages != null && pages.Length > 1;
|
||||
if (previousPageButton != null)
|
||||
{
|
||||
previousPageButton.gameObject.SetActive(hasPages);
|
||||
previousPageButton.Interactable(hasPages && pageIndex > 0);
|
||||
}
|
||||
|
||||
if (nextPageButton != null)
|
||||
{
|
||||
nextPageButton.gameObject.SetActive(hasPages);
|
||||
nextPageButton.Interactable(hasPages && pageIndex < pages.Length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void Hide(bool immediate)
|
||||
{
|
||||
if (popupImageRect == null) return;
|
||||
|
||||
sequence?.Kill();
|
||||
sequence = null;
|
||||
|
||||
if (immediate)
|
||||
{
|
||||
popupImageRect.anchoredPosition = new Vector2(popupImageRect.anchoredPosition.x, hiddenY);
|
||||
popupImageRect.gameObject.SetActive(false);
|
||||
gameObject.SetActive(false);
|
||||
if (lockPlayerControl && PlayerControlLockService.Instance != null)
|
||||
{
|
||||
PlayerControlLockService.Instance.Unlock(this);
|
||||
}
|
||||
isOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
sequence = DOTween.Sequence();
|
||||
sequence.Append(popupImageRect.DOAnchorPosY(hiddenY, tweenDuration).SetEase(Ease.InCubic));
|
||||
sequence.AppendCallback(() =>
|
||||
{
|
||||
popupImageRect.gameObject.SetActive(false);
|
||||
gameObject.SetActive(false);
|
||||
if (lockPlayerControl && PlayerControlLockService.Instance != null)
|
||||
{
|
||||
PlayerControlLockService.Instance.Unlock(this);
|
||||
}
|
||||
isOpen = false;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (!isOpen) return;
|
||||
sequence?.Kill();
|
||||
sequence = null;
|
||||
if (lockPlayerControl && PlayerControlLockService.Instance != null)
|
||||
{
|
||||
PlayerControlLockService.Instance.Unlock(this);
|
||||
}
|
||||
isOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd01cdbd042422d4a957d1efb4cdd596
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,214 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using LLM; // 引用新的 LLM 命名空间
|
||||
using Core.InputLock;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class TabletController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private bool debugLogs = true;
|
||||
[Header("UI References")]
|
||||
public GameObject tabletPanel; // 整个平板界面的父物体
|
||||
public TMP_InputField inputField; // 输入框
|
||||
public Transform chatContent; // 聊天记录的父物体 (ScrollView Content)
|
||||
public GameObject messagePrefab; // 聊天气泡预设体
|
||||
|
||||
[Header("Game Control")]
|
||||
public GameObject hudCanvas; // 游戏原本的 HUD (准星等),打开平板时隐藏
|
||||
public MonoBehaviour playerController; // 玩家控制器脚本
|
||||
|
||||
[Header("LLM Manager")]
|
||||
public LLMChatManager chatManager; // 引用 LLMChatManager 来管理多模型切换
|
||||
// public GameObject llmServiceObj; // 已废弃,请使用 chatManager
|
||||
|
||||
private Coroutine rebuildChatLayoutRoutine;
|
||||
|
||||
public static TabletController Instance { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null) Instance = this;
|
||||
else Destroy(gameObject);
|
||||
|
||||
if (tabletPanel != null) tabletPanel.SetActive(false);
|
||||
|
||||
if (playerController == null)
|
||||
{
|
||||
playerController = FindObjectOfType<Player.PlayerController>(true);
|
||||
}
|
||||
|
||||
// 自动查找 LLMChatManager (如果在场景中)
|
||||
if (chatManager == null)
|
||||
{
|
||||
chatManager = FindObjectOfType<LLMChatManager>();
|
||||
}
|
||||
|
||||
if (chatManager == null)
|
||||
{
|
||||
Debug.LogError("[TabletController] 未找到 LLMChatManager!请确保场景中存在该管理器。");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 监听历史记录加载完成(这里简单地在 Start 中延迟一帧加载,因为 SaveManager 加载通常很快)
|
||||
StartCoroutine(InitChatHistory());
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator InitChatHistory()
|
||||
{
|
||||
// 等待一帧,确保 LLMChatManager 已经完成了 Start 中的 SaveManager.LoadGame()
|
||||
yield return null;
|
||||
|
||||
if (chatManager.fullChatLog != null && chatManager.fullChatLog.Count > 0)
|
||||
{
|
||||
// 清理可能存在的测试数据(如果有)
|
||||
foreach (Transform child in chatContent)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
|
||||
// 重建 UI
|
||||
foreach (var msg in chatManager.fullChatLog)
|
||||
{
|
||||
bool isPlayer = (msg.role == "user");
|
||||
// 只有 user 和 assistant 的消息需要显示
|
||||
if (msg.role == "user" || msg.role == "assistant")
|
||||
{
|
||||
AddMessageToUI(isPlayer ? "Player" : "AI", msg.content, isPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
yield return new WaitForEndOfFrame();
|
||||
ScrollRect scrollRect = chatContent.GetComponentInParent<ScrollRect>();
|
||||
if (scrollRect != null) scrollRect.verticalNormalizedPosition = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowTablet()
|
||||
{
|
||||
if (debugLogs) Debug.Log("[TabletController] ShowTablet");
|
||||
if (tabletPanel != null) tabletPanel.SetActive(true);
|
||||
if (hudCanvas != null) hudCanvas.SetActive(false);
|
||||
|
||||
TogglePlayerControl(false);
|
||||
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
|
||||
if (inputField != null) inputField.ActivateInputField();
|
||||
|
||||
if (rebuildChatLayoutRoutine != null) StopCoroutine(rebuildChatLayoutRoutine);
|
||||
rebuildChatLayoutRoutine = StartCoroutine(RebuildChatLayoutNextFrame());
|
||||
|
||||
if (UI.PanelStack.UIPanelStack.Instance != null && tabletPanel != null)
|
||||
{
|
||||
UI.PanelStack.UIPanelStack.Instance.Push(UI.PanelStack.UIPanelKind.Custom, tabletPanel, HideTablet);
|
||||
}
|
||||
}
|
||||
|
||||
public void HideTablet()
|
||||
{
|
||||
if (debugLogs) Debug.Log("[TabletController] HideTablet");
|
||||
if (tabletPanel != null) tabletPanel.SetActive(false);
|
||||
if (hudCanvas != null) hudCanvas.SetActive(true);
|
||||
|
||||
TogglePlayerControl(true);
|
||||
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
Cursor.visible = false;
|
||||
|
||||
if (UI.PanelStack.UIPanelStack.Instance != null && tabletPanel != null)
|
||||
{
|
||||
UI.PanelStack.UIPanelStack.Instance.Remove(tabletPanel);
|
||||
}
|
||||
}
|
||||
|
||||
private void TogglePlayerControl(bool enable)
|
||||
{
|
||||
if (PlayerControlLockService.Instance != null)
|
||||
{
|
||||
if (enable) { PlayerControlLockService.Instance.Unlock(this); }
|
||||
else { PlayerControlLockService.Instance.Lock(this); }
|
||||
}
|
||||
|
||||
if (playerController != null) playerController.enabled = enable;
|
||||
}
|
||||
|
||||
public void OnSendClicked()
|
||||
{
|
||||
if (inputField == null || string.IsNullOrWhiteSpace(inputField.text)) return;
|
||||
if (chatManager == null)
|
||||
{
|
||||
AddMessageToUI("System", "LLM 服务管理器未连接。", true);
|
||||
return;
|
||||
}
|
||||
|
||||
string userMessage = inputField.text;
|
||||
inputField.text = "";
|
||||
|
||||
AddMessageToUI("Player", userMessage, true);
|
||||
|
||||
// 使用 LLMChatManager 发送消息,它会自动选择当前激活的服务
|
||||
chatManager.SendUserMessage(userMessage, (reply, success) =>
|
||||
{
|
||||
AddMessageToUI("AI", reply, false);
|
||||
});
|
||||
}
|
||||
|
||||
private void AddMessageToUI(string role, string text, bool isPlayer)
|
||||
{
|
||||
if (messagePrefab == null || chatContent == null) return;
|
||||
|
||||
GameObject newMsg = Instantiate(messagePrefab, chatContent);
|
||||
TextMeshProUGUI tmp = newMsg.GetComponentInChildren<TextMeshProUGUI>();
|
||||
|
||||
if (tmp != null)
|
||||
{
|
||||
tmp.text = text;
|
||||
}
|
||||
|
||||
var aligner = newMsg.GetComponent<MessageAligner>();
|
||||
if (aligner != null)
|
||||
{
|
||||
aligner.SetAlignment(isPlayer);
|
||||
}
|
||||
|
||||
// 限制 UI 显示数量,防止无限增加导致卡顿 (保留最近的 50 条)
|
||||
int maxUIItems = 50;
|
||||
if (chatContent.childCount > maxUIItems)
|
||||
{
|
||||
Destroy(chatContent.GetChild(0).gameObject);
|
||||
}
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
ScrollRect scrollRect = chatContent.GetComponentInParent<ScrollRect>();
|
||||
if (scrollRect != null) scrollRect.verticalNormalizedPosition = 0f;
|
||||
}
|
||||
|
||||
private IEnumerator RebuildChatLayoutNextFrame()
|
||||
{
|
||||
yield return new WaitForEndOfFrame();
|
||||
|
||||
if (chatContent == null) yield break;
|
||||
|
||||
foreach (Transform child in chatContent)
|
||||
{
|
||||
var aligner = child.GetComponent<MessageAligner>();
|
||||
if (aligner != null) aligner.Reapply();
|
||||
}
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
|
||||
RectTransform chatContentRect = chatContent as RectTransform;
|
||||
if (chatContentRect != null) LayoutRebuilder.ForceRebuildLayoutImmediate(chatContentRect);
|
||||
|
||||
ScrollRect scrollRect = chatContent.GetComponentInParent<ScrollRect>();
|
||||
if (scrollRect != null) scrollRect.verticalNormalizedPosition = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a67e02611313a00458ff66885ea94f85
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0f1e2d3c4b5a69788796a5b4c3d2e1f0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using Core.TaskSystem;
|
||||
using Michsky.UI.Reach;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace UI.Tasks
|
||||
{
|
||||
public class TaskListItemView : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private ButtonManager button;
|
||||
|
||||
[Header("Status Presentation")]
|
||||
[SerializeField] private Sprite todoIcon;
|
||||
[SerializeField] private Sprite completedIcon;
|
||||
[SerializeField] private Sprite expiredIcon;
|
||||
|
||||
[SerializeField] private string todoPrefix = "";
|
||||
[SerializeField] private string completedPrefix = "";
|
||||
[SerializeField] private string expiredPrefix = "";
|
||||
|
||||
TaskEntry task;
|
||||
Action<TaskEntry> onClicked;
|
||||
UnityAction clickAction;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (button == null) { button = GetComponent<ButtonManager>(); }
|
||||
}
|
||||
|
||||
public void Bind(TaskEntry task, Action<TaskEntry> onClicked)
|
||||
{
|
||||
this.task = task;
|
||||
this.onClicked = onClicked;
|
||||
|
||||
if (button == null)
|
||||
{
|
||||
Debug.LogError($"{nameof(TaskListItemView)} on '{name}' is missing ButtonManager reference.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (clickAction != null) { button.onClick.RemoveListener(clickAction); }
|
||||
clickAction = HandleClick;
|
||||
button.onClick.AddListener(clickAction);
|
||||
|
||||
ApplyPresentation(task);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (button != null && clickAction != null) { button.onClick.RemoveListener(clickAction); }
|
||||
}
|
||||
|
||||
void HandleClick()
|
||||
{
|
||||
onClicked?.Invoke(task);
|
||||
}
|
||||
|
||||
void ApplyPresentation(TaskEntry task)
|
||||
{
|
||||
if (task == null)
|
||||
{
|
||||
if (button != null) { button.SetText(string.Empty); }
|
||||
return;
|
||||
}
|
||||
|
||||
(Sprite icon, string prefix) = task.status switch
|
||||
{
|
||||
TaskStatus.Completed => (completedIcon, completedPrefix),
|
||||
TaskStatus.Expired => (expiredIcon, expiredPrefix),
|
||||
_ => (todoIcon, todoPrefix)
|
||||
};
|
||||
|
||||
button.enableIcon = icon != null;
|
||||
button.buttonIcon = icon;
|
||||
|
||||
string label = string.IsNullOrEmpty(prefix) ? task.ShortName : $"{prefix}{task.ShortName}";
|
||||
button.SetText(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f1e2d3c5b6a4d7e8f9a0b1c2d3e4f5a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
using Core.TaskSystem;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UI.Tasks
|
||||
{
|
||||
public class TaskPanelController : MonoBehaviour
|
||||
{
|
||||
[Header("System")]
|
||||
[SerializeField] private TaskService taskService;
|
||||
|
||||
[Header("List")]
|
||||
[SerializeField] private RectTransform listContent;
|
||||
[SerializeField] private TaskListItemView taskItemPrefab;
|
||||
|
||||
[Header("Details")]
|
||||
[SerializeField] private TMP_Text detailText;
|
||||
[SerializeField] private TMP_Text titleText;
|
||||
|
||||
[Header("Selection")]
|
||||
[SerializeField] private bool autoSelectFirstTask = true;
|
||||
|
||||
TaskService boundService;
|
||||
string selectedTaskId;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
BindService();
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
UnbindService();
|
||||
}
|
||||
|
||||
void BindService()
|
||||
{
|
||||
TaskService service = taskService != null ? taskService : TaskService.Instance;
|
||||
if (service == null)
|
||||
{
|
||||
Debug.LogError($"{nameof(TaskPanelController)} requires a TaskService reference.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (boundService == service) { return; }
|
||||
UnbindService();
|
||||
|
||||
boundService = service;
|
||||
boundService.Changed += RefreshAll;
|
||||
}
|
||||
|
||||
void UnbindService()
|
||||
{
|
||||
if (boundService == null) { return; }
|
||||
boundService.Changed -= RefreshAll;
|
||||
boundService = null;
|
||||
}
|
||||
|
||||
void RefreshAll()
|
||||
{
|
||||
if (boundService == null) { return; }
|
||||
if (listContent == null || taskItemPrefab == null) { return; }
|
||||
|
||||
for (int i = listContent.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
Destroy(listContent.GetChild(i).gameObject);
|
||||
}
|
||||
|
||||
for (int i = 0; i < boundService.Tasks.Count; i++)
|
||||
{
|
||||
TaskEntry task = boundService.Tasks[i];
|
||||
TaskListItemView view = Instantiate(taskItemPrefab, listContent);
|
||||
view.Bind(task, OnTaskClicked);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(selectedTaskId) && boundService.TryGetTask(selectedTaskId, out TaskEntry selected))
|
||||
{
|
||||
ShowDetails(selected);
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoSelectFirstTask && boundService.Tasks.Count > 0)
|
||||
{
|
||||
Select(boundService.Tasks[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
ClearDetails();
|
||||
}
|
||||
|
||||
void OnTaskClicked(TaskEntry task)
|
||||
{
|
||||
Select(task);
|
||||
}
|
||||
|
||||
void Select(TaskEntry task)
|
||||
{
|
||||
if (task == null)
|
||||
{
|
||||
selectedTaskId = null;
|
||||
ClearDetails();
|
||||
return;
|
||||
}
|
||||
|
||||
selectedTaskId = task.id;
|
||||
ShowDetails(task);
|
||||
}
|
||||
|
||||
void ShowDetails(TaskEntry task)
|
||||
{
|
||||
if (titleText != null) { titleText.text = task.ShortName; }
|
||||
|
||||
if (detailText != null)
|
||||
{
|
||||
string statusLabel = task.status switch
|
||||
{
|
||||
TaskStatus.Completed => "完成",
|
||||
TaskStatus.Expired => "超时",
|
||||
_ => "待做"
|
||||
};
|
||||
|
||||
detailText.text = $"{statusLabel}\n\n{task.Description}";
|
||||
}
|
||||
}
|
||||
|
||||
void ClearDetails()
|
||||
{
|
||||
if (titleText != null) { titleText.text = string.Empty; }
|
||||
if (detailText != null) { detailText.text = string.Empty; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b9c1d2e3f4a5b6c7d8e9f0a1b2c3d4e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9418e8fec633d2c4983ebb1c61ac4238
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,199 @@
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using Michsky.UI.Reach;
|
||||
using UI.Settings;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace UI.Title
|
||||
{
|
||||
public class TitleMenuController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private bool debugLogs = true;
|
||||
|
||||
[Header("Scene")]
|
||||
[SerializeField] private int nextSceneBuildIndex = 2;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField] private ButtonManager startOrRestartButton;
|
||||
[SerializeField] private ButtonManager continueButton;
|
||||
[SerializeField] private ButtonManager settingsButton;
|
||||
[SerializeField] private ButtonManager quitButton;
|
||||
|
||||
[Header("Settings Panel")]
|
||||
[SerializeField] private SettingsController settingsController;
|
||||
|
||||
[Header("Labels")]
|
||||
[SerializeField] private string loadingText = "正在加载";
|
||||
[SerializeField] private string startText = "开始游戏";
|
||||
[SerializeField] private string restartText = "重新开始";
|
||||
[SerializeField] private string continueText = "继续游戏";
|
||||
|
||||
private AsyncOperation loadOperation;
|
||||
private bool isWired;
|
||||
private bool hasSave;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
|
||||
if (settingsController == null)
|
||||
{
|
||||
settingsController = FindObjectOfType<SettingsController>(true);
|
||||
}
|
||||
|
||||
if (debugLogs) Debug.Log($"[TitleMenu] Awake. settingsController={(settingsController!=null)} nextSceneBuildIndex={nextSceneBuildIndex}");
|
||||
RefreshSaveState();
|
||||
WireOnce();
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
BeginPreload();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
|
||||
RefreshSaveState();
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
public void BeginPreload()
|
||||
{
|
||||
if (loadOperation != null) { return; }
|
||||
|
||||
if (nextSceneBuildIndex < 0 || nextSceneBuildIndex >= SceneManager.sceneCountInBuildSettings)
|
||||
{
|
||||
if (debugLogs) Debug.Log($"[TitleMenu] Invalid nextSceneBuildIndex={nextSceneBuildIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (debugLogs) Debug.Log($"[TitleMenu] BeginPreload buildIndex={nextSceneBuildIndex}");
|
||||
StartCoroutine(PreloadRoutine());
|
||||
}
|
||||
|
||||
private IEnumerator PreloadRoutine()
|
||||
{
|
||||
loadOperation = SceneManager.LoadSceneAsync(nextSceneBuildIndex);
|
||||
if (loadOperation == null) { yield break; }
|
||||
|
||||
loadOperation.allowSceneActivation = false;
|
||||
|
||||
while (loadOperation.progress < 0.9f)
|
||||
{
|
||||
UpdateButtons();
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (debugLogs) Debug.Log("[TitleMenu] Preload ready (progress>=0.9).");
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
private bool IsPreloadReady()
|
||||
{
|
||||
return loadOperation != null && loadOperation.progress >= 0.9f;
|
||||
}
|
||||
|
||||
private void WireOnce()
|
||||
{
|
||||
if (isWired) { return; }
|
||||
|
||||
if (startOrRestartButton != null) { startOrRestartButton.onClick.AddListener(OnStartOrRestartClicked); }
|
||||
if (continueButton != null) { continueButton.onClick.AddListener(OnContinueClicked); }
|
||||
if (settingsButton != null) { settingsButton.onClick.AddListener(OnSettingsClicked); }
|
||||
if (quitButton != null) { quitButton.onClick.AddListener(OnQuitClicked); }
|
||||
|
||||
isWired = true;
|
||||
}
|
||||
|
||||
private void RefreshSaveState()
|
||||
{
|
||||
string savePath = Path.Combine(Application.persistentDataPath, "savegame.json");
|
||||
hasSave = File.Exists(savePath);
|
||||
}
|
||||
|
||||
private void UpdateButtons()
|
||||
{
|
||||
bool preloadReady = IsPreloadReady();
|
||||
|
||||
if (startOrRestartButton != null)
|
||||
{
|
||||
if (!preloadReady)
|
||||
{
|
||||
startOrRestartButton.SetText(loadingText);
|
||||
startOrRestartButton.Interactable(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
startOrRestartButton.SetText(hasSave ? restartText : startText);
|
||||
startOrRestartButton.Interactable(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (continueButton != null)
|
||||
{
|
||||
continueButton.gameObject.SetActive(hasSave);
|
||||
|
||||
if (hasSave)
|
||||
{
|
||||
if (!preloadReady)
|
||||
{
|
||||
continueButton.SetText(loadingText);
|
||||
continueButton.Interactable(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
continueButton.SetText(continueText);
|
||||
continueButton.Interactable(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStartOrRestartClicked()
|
||||
{
|
||||
if (!IsPreloadReady()) { return; }
|
||||
|
||||
if (hasSave)
|
||||
{
|
||||
string savePath = Path.Combine(Application.persistentDataPath, "savegame.json");
|
||||
if (File.Exists(savePath)) { File.Delete(savePath); }
|
||||
hasSave = false;
|
||||
}
|
||||
|
||||
ActivateLoadedScene();
|
||||
}
|
||||
|
||||
private void OnContinueClicked()
|
||||
{
|
||||
if (!hasSave) { return; }
|
||||
if (!IsPreloadReady()) { return; }
|
||||
ActivateLoadedScene();
|
||||
}
|
||||
|
||||
private void ActivateLoadedScene()
|
||||
{
|
||||
if (loadOperation == null) { return; }
|
||||
loadOperation.allowSceneActivation = true;
|
||||
}
|
||||
|
||||
private void OnSettingsClicked()
|
||||
{
|
||||
if (debugLogs) Debug.Log($"[TitleMenu] Settings clicked. settingsController={(settingsController!=null)}");
|
||||
if (settingsController != null)
|
||||
{
|
||||
settingsController.OpenAtIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnQuitClicked()
|
||||
{
|
||||
Application.Quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43fe25381a0d0e2458ecc33002816a46
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,80 @@
|
||||
using Michsky.UI.Reach;
|
||||
using Player;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class VitalsHUD : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
public PlayerVitalsSystem target;
|
||||
[FormerlySerializedAs("healthBar")] public ProgressBar thirstBar;
|
||||
public ProgressBar hungerBar;
|
||||
public ProgressBar sanityBar;
|
||||
|
||||
bool isBound;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
target = FindObjectOfType<PlayerVitalsSystem>();
|
||||
}
|
||||
|
||||
Bind();
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
Unbind();
|
||||
}
|
||||
|
||||
void Bind()
|
||||
{
|
||||
if (isBound) return;
|
||||
if (target == null) return;
|
||||
|
||||
target.health.onValueChanged.AddListener(OnThirstChanged);
|
||||
target.hunger.onValueChanged.AddListener(OnHungerChanged);
|
||||
target.sanity.onValueChanged.AddListener(OnSanityChanged);
|
||||
|
||||
isBound = true;
|
||||
}
|
||||
|
||||
void Unbind()
|
||||
{
|
||||
if (!isBound) return;
|
||||
if (target == null) return;
|
||||
|
||||
target.health.onValueChanged.RemoveListener(OnThirstChanged);
|
||||
target.hunger.onValueChanged.RemoveListener(OnHungerChanged);
|
||||
target.sanity.onValueChanged.RemoveListener(OnSanityChanged);
|
||||
|
||||
isBound = false;
|
||||
}
|
||||
|
||||
void RefreshAll()
|
||||
{
|
||||
if (target == null) return;
|
||||
UpdateBar(thirstBar, target.health);
|
||||
UpdateBar(hungerBar, target.hunger);
|
||||
UpdateBar(sanityBar, target.sanity);
|
||||
}
|
||||
|
||||
void UpdateBar(ProgressBar bar, VitalStat stat)
|
||||
{
|
||||
if (bar == null) return;
|
||||
if (stat == null) return;
|
||||
|
||||
bar.minValue = stat.minValue;
|
||||
bar.maxValue = stat.maxValue;
|
||||
bar.SetValue(stat.currentValue);
|
||||
}
|
||||
|
||||
void OnThirstChanged(float _) => UpdateBar(thirstBar, target.health);
|
||||
void OnHungerChanged(float _) => UpdateBar(hungerBar, target.hunger);
|
||||
void OnSanityChanged(float _) => UpdateBar(sanityBar, target.sanity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95315142015562d45bb3c53430aa32ab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user