Initial Unity project commit

This commit is contained in:
2026-07-08 19:53:15 +08:00
commit 75f254212e
15657 changed files with 11633879 additions and 0 deletions
@@ -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:
+412
View File
@@ -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:
+8
View File
@@ -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:
+9
View File
@@ -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:
+8
View File
@@ -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:
+80
View File
@@ -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: