Files
AIGame/AIClient/Assets/HotUpdate/Scripts/UI/MainHudView.cs
T

615 lines
27 KiB
C#

using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace HotUpdate.UI
{
/// <summary>
/// 营地主界面:绑定 JSON Prefab 入口、网络状态和断线重连。
/// </summary>
public sealed class MainHudView : MonoBehaviour
{
public const string PrefabName = "UI_MainHUD";
TextMeshProUGUI _status;
TextMeshProUGUI _pid;
GameObject _reconnectPanel;
TextMeshProUGUI _reconnectText;
Button _reconnectBtn;
HomeCampPresenter _campPresenter;
public event Action OnReconnectClicked;
public event Action<string> OnHomeActionClicked;
public void TriggerHomeAction(string key)
{
OnHomeActionClicked?.Invoke(key);
}
public void Bind()
{
var campWorld = transform.Find("CampWorld");
if (campWorld != null)
{
_campPresenter = campWorld.GetComponent<HomeCampPresenter>();
if (_campPresenter == null)
_campPresenter = campWorld.gameObject.AddComponent<HomeCampPresenter>();
_campPresenter.SetOwner(this);
}
_status = FindTmp("Txt_Status");
_pid = FindTmp("Txt_Pid");
var panel = transform.Find("Panel_Reconnect");
_reconnectPanel = panel != null ? panel.gameObject : null;
_reconnectText = FindTmp("Txt_Reconnect");
var btnTf = transform.Find("Panel_Reconnect/Btn_Reconnect");
_reconnectBtn = btnTf != null ? btnTf.GetComponent<Button>() : null;
if (_reconnectBtn != null)
{
_reconnectBtn.onClick.RemoveAllListeners();
_reconnectBtn.onClick.AddListener(() => OnReconnectClicked?.Invoke());
}
ShowReconnect(false);
foreach (var button in GetComponentsInChildren<Button>(true))
{
if (button == _reconnectBtn || button.name == "Btn_Explorer") continue;
string key = null;
string bRaw = null;
if (button.name.StartsWith("Btn_Nav", StringComparison.Ordinal))
key = button.name.Substring(7);
else if (button.name.StartsWith("Btn_Building_", StringComparison.Ordinal))
{
bRaw = button.name.Substring(13);
if (bRaw == "MainHouse") key = "Camp";
else if (bRaw == "Fountain") key = "Rewards";
else if (bRaw == "Stall") key = "Daily";
else key = bRaw;
}
else if (button.name.StartsWith("Btn_", StringComparison.Ordinal))
key = button.name.Substring(4);
if (key == null) continue;
string actionKey = key;
string buildingRaw = bRaw;
button.onClick.RemoveAllListeners();
button.onClick.AddListener(() =>
{
if (!string.IsNullOrEmpty(buildingRaw) && _campPresenter != null && _campPresenter.HasExplorer)
{
_campPresenter.NavigateToBuilding(buildingRaw, actionKey, () => OnHomeActionClicked?.Invoke(actionKey));
}
else
{
OnHomeActionClicked?.Invoke(actionKey);
}
});
}
}
public void SetStatus(string text)
{
if (_status != null) _status.text = text ?? string.Empty;
}
public void SetPid(long pid)
{
if (_pid != null) _pid.text = pid > 0 ? $"pid={pid}" : "pid=-";
}
public void SetPrototypeState(Gameplay.PrototypeSave state, Gameplay.PrototypeConfig config = null, long serverNow = 0)
{
SetText("Txt_Level", "Lv. " + state.level);
SetText("Txt_Coin", state.coins.ToString("N0"));
SetText("Txt_Diamond", "0");
SetText("Txt_Pinballs", state.balls.ToString());
SetText("Txt_TaskCaption", "教学目标");
SetText("Txt_Task", GuideTask(state, config));
SetText("Txt_AdventureHint", state.cleared ? "首通已完成 · 可重复挑战" : "首通另获糖果 40 · 不消耗体力");
if (config != null && state.schema >= 2)
{
var stage = config.Stage(state.UnlockedStage); bool complete = state.clearedStages[stage.id - 1] == 1;
SetText("Txt_Chapter", config.chapter);
SetText("Txt_Stage", stage.code + " · " + stage.name);
SetText("Txt_Reward_coin", complete ? "已领取" : stage.reward.coins.ToString());
SetText("Txt_Reward_pinball", complete ? "已领取" : stage.reward.balls.ToString());
SetText("Txt_AdventureHint", complete ? "本轮五关已完成 · 可免费重试" : "首通另获糖果 " + stage.reward.candy + " · 不消耗体力");
}
foreach (var child in GetComponentsInChildren<Transform>(true))
if (child.name.StartsWith("Dot_", StringComparison.Ordinal)) child.gameObject.SetActive(false);
if (state.mails != null)
foreach (var child in GetComponentsInChildren<Transform>(true))
if (child.name == "Dot_Mail") child.gameObject.SetActive(Array.Exists(state.mails, m => !m.expired && serverNow < m.expiresAt && (!m.read || !m.claimed && m.HasAttachment)));
if (config?.rewards?.chests != null && state.chests != null)
foreach (var child in GetComponentsInChildren<Transform>(true))
if (child.name == "Dot_Rewards") child.gameObject.SetActive(state.chestDailyClaimed == 0 || Array.Exists(state.chests, n => n > 0));
if (state.schema >= 5 && config?.daily != null)
{
int ready = 0; bool signin = false;
for (int i = 0; i < 3; i++) if (state.dailyClaimed[i] == 0 && state.dailyProgress[i] >= config.daily.tasks[i].target) ready++;
for (int i = 0; i < state.signinDays; i++) if (state.signinClaimed[i] == 0) signin = true;
foreach (var child in GetComponentsInChildren<Transform>(true))
if (child.name == "Dot_Tasks") child.gameObject.SetActive(ready > 0 || state.cleared && !state.goalClaimed);
else if (child.name == "Dot_Daily") child.gameObject.SetActive(signin);
if (state.goalClaimed)
{
SetText("Txt_TaskCaption", "每日目标");
SetText("Txt_Task", ready > 0 ? ready + "项奖励可领取" : "查看今日任务进度");
}
}
}
public static string GuideTask(Gameplay.PrototypeSave state, Gameplay.PrototypeConfig config = null)
{
if (state.goalClaimed) return "教学目标已完成";
if (state.cleared) return "领取首通目标奖励";
if (state.shots == 0) return "发射第一枚弹珠";
if (!state.captureTutorialDone && state.tickets > 0) return "用捕捉券获得芽芽";
if (state.level < 2)
{
int cost = (config?.economy?.trainCost ?? 30) * state.level;
if (state.candy < cost) return state.balls > 0 ? "发射弹珠,补足训练糖果" : "领取免费补给,再收集训练糖果";
return "用糖果做一次共享训练";
}
return "布阵并完成教学关 1-1";
}
void SetText(string name, string value)
{
var label = FindTmp(name);
if (label != null) label.text = value;
}
public void ShowReconnect(bool visible, string message = null)
{
if (_reconnectPanel != null) _reconnectPanel.SetActive(visible);
if (visible && _reconnectText != null && !string.IsNullOrEmpty(message))
_reconnectText.text = message;
}
TextMeshProUGUI FindTmp(string path)
{
var t = transform.Find(path);
if (t == null)
{
// 深层查找
var all = GetComponentsInChildren<TextMeshProUGUI>(true);
foreach (var c in all)
if (c.name == path) return c;
return null;
}
return t.GetComponent<TextMeshProUGUI>();
}
}
/// <summary>Displays the authored camp scene behind the existing HUD.</summary>
public sealed class HomeCampPresenter : MonoBehaviour
{
const string ScenePath = "Assets/Bundles/Scenes/Town/HomeCamp/HomeCamp.unity";
const float ReferenceAspect = 1080f / 1920f;
RawImage _image;
Camera _camera;
GameObject _sceneRoot;
Transform _backdrop;
RenderTexture _texture;
YooAsset.SceneHandle _handle;
bool _loading, _destroyed;
GameObject _explorerStage, _explorerButton;
// Building transform references and UI bubble anchors
Transform _mainHouse, _houseLeft, _houseRight, _fountain, _stall;
RectTransform _bubbleMainHouse, _bubbleShop, _bubblePets, _bubbleFountain, _bubbleStall;
// Protagonist Explorer references
Transform _explorer;
HotUpdate.Gameplay.CampExplorerController _explorerController;
MainHudView _owner;
public void SetOwner(MainHudView owner) => _owner = owner;
public bool HasExplorer => _explorerController != null;
struct BuildingEntrance
{
public Vector3 position;
public Vector3 facing;
}
static readonly System.Collections.Generic.Dictionary<string, BuildingEntrance> BuildingEntrances =
new System.Collections.Generic.Dictionary<string, BuildingEntrance>(StringComparer.OrdinalIgnoreCase)
{
{ "MainHouse", new BuildingEntrance { position = new Vector3(0f, 0f, 3.5f), facing = Vector3.forward } },
{ "Shop", new BuildingEntrance { position = new Vector3(-3.8f, 0f, 1.8f), facing = new Vector3(-1f, 0f, 0.3f) } },
{ "HouseLeft", new BuildingEntrance { position = new Vector3(-3.8f, 0f, 1.8f), facing = new Vector3(-1f, 0f, 0.3f) } },
{ "Pets", new BuildingEntrance { position = new Vector3(3.8f, 0f, 1.8f), facing = new Vector3(1f, 0f, 0.3f) } },
{ "HouseRight", new BuildingEntrance { position = new Vector3(3.8f, 0f, 1.8f), facing = new Vector3(1f, 0f, 0.3f) } },
{ "Fountain", new BuildingEntrance { position = new Vector3(0f, 0f, -2.6f), facing = Vector3.forward } },
{ "Stall", new BuildingEntrance { position = new Vector3(-2.8f, 0f, -3.2f), facing = Vector3.left } },
};
string _currentNavBuilding;
public void NavigateToBuilding(string buildingKey, string actionKey, Action onArrive)
{
if (_explorerController == null)
{
onArrive?.Invoke();
return;
}
// 再次点击同一建筑则立即快速进入
if (_currentNavBuilding == buildingKey && _explorerController.IsMoving)
{
_currentNavBuilding = null;
_explorerController.Stop();
onArrive?.Invoke();
return;
}
if (BuildingEntrances.TryGetValue(buildingKey, out var entrance))
{
_currentNavBuilding = buildingKey;
_explorerController.MoveTo(entrance.position, () =>
{
_currentNavBuilding = null;
onArrive?.Invoke();
}, entrance.facing);
}
else
{
onArrive?.Invoke();
}
}
public bool Ready => _camera != null && _texture != null;
public bool Loading => _loading;
void Awake()
{
_image = GetComponent<RawImage>();
if (_image != null) _image.enabled = false;
}
void OnEnable()
{
if (_sceneRoot != null) { _sceneRoot.SetActive(true); Resize(); UpdateBuildingAnchors(); }
else if (!_loading) Load();
}
void OnDisable()
{
if (_sceneRoot != null) _sceneRoot.SetActive(false);
if (_image != null) _image.enabled = false;
}
void Load()
{
_loading = true;
if (YooAsset.YooAssets.IsInitialized && YooAsset.YooAssets.TryGetPackage("Art", out var package) &&
package.InitializeStatus == YooAsset.EOperationStatus.Succeeded && package.IsLocationValid("HomeCamp"))
{
_handle = package.LoadSceneAsync("HomeCamp", UnityEngine.SceneManagement.LoadSceneMode.Additive);
// The shipped October shell stripped SceneHandle.Completed and UnloadSceneAsync.
// Poll inherited HandleBase members on the persistent UI host so hiding the HUD
// during a download does not stop completion/cleanup.
MonoBehaviour host = UiManager.Instance != null ? (MonoBehaviour)UiManager.Instance : this;
host.StartCoroutine(WaitForScene(_handle));
return;
}
#if UNITY_EDITOR
var operation = UnityEditor.SceneManagement.EditorSceneManager.LoadSceneAsyncInPlayMode(
ScenePath, new UnityEngine.SceneManagement.LoadSceneParameters(UnityEngine.SceneManagement.LoadSceneMode.Additive));
if (operation != null)
{
operation.completed += _ => {
var scene = UnityEngine.SceneManagement.SceneManager.GetSceneByPath(ScenePath);
if (_destroyed) { if (scene.IsValid() && scene.isLoaded) UnityEngine.SceneManagement.SceneManager.UnloadSceneAsync(scene); return; }
Setup(scene);
};
return;
}
#endif
_loading = false;
Debug.LogWarning("[HomeCamp] Scene unavailable in Art; retaining the illustrated background.");
}
System.Collections.IEnumerator WaitForScene(YooAsset.SceneHandle handle)
{
while (YooAsset.YooAssets.IsInitialized && handle.IsValid && !handle.IsDone) yield return null;
if (YooAsset.YooAssets.IsInitialized && handle.IsValid) Loaded(handle);
}
static void ReleaseScene(YooAsset.SceneHandle handle)
{
if (!YooAsset.YooAssets.IsInitialized || !handle.IsValid) return;
if (handle.Status == YooAsset.EOperationStatus.Succeeded)
{
var scene = handle.SceneObject;
// YooAsset listens to sceneUnloaded and releases its scene handles there.
// Using Unity's retained API also works with the existing AOT shell.
if (scene.IsValid() && scene.isLoaded)
{
UnityEngine.SceneManagement.SceneManager.UnloadSceneAsync(scene);
return;
}
}
handle.Release();
}
void Loaded(YooAsset.SceneHandle handle)
{
if (_destroyed) { ReleaseScene(handle); return; }
if (handle.Status != YooAsset.EOperationStatus.Succeeded)
{
Debug.LogWarning("[HomeCamp] Scene load failed: " + handle.Error);
handle.Release(); _handle = null; _loading = false; return;
}
Setup(handle.SceneObject);
}
void Setup(UnityEngine.SceneManagement.Scene scene)
{
_loading = false;
if (scene.IsValid() && scene.isLoaded)
foreach (var root in scene.GetRootGameObjects())
if (root.name == "HomeCamp") { _sceneRoot = root; break; }
if (_sceneRoot != null)
{
_camera = _sceneRoot.GetComponentInChildren<Camera>(true);
_backdrop = _sceneRoot.transform.Find("DistantLakeWaterfallCastle");
_mainHouse = _sceneRoot.transform.Find("MainHouse");
_houseLeft = _sceneRoot.transform.Find("HouseLeft");
_houseRight = _sceneRoot.transform.Find("HouseRight");
_fountain = _sceneRoot.transform.Find("Fountain");
_stall = _sceneRoot.transform.Find("StallRight");
}
if (_camera == null || _image == null)
{
Debug.LogWarning("[HomeCamp] Scene camera missing; retaining the illustrated background.");
if (_sceneRoot != null) _sceneRoot.SetActive(false);
return;
}
_camera.enabled = false;
// The old guide is a separate 2D camera overlay and does not match the new scene's scale or ground.
_explorerStage = transform.parent.Find("ExplorerStage")?.gameObject;
_explorerButton = transform.parent.Find("Btn_Explorer")?.gameObject;
if (_explorerStage != null) _explorerStage.SetActive(false);
if (_explorerButton != null) _explorerButton.SetActive(false);
BindBuildingBubbles();
// Bind Explorer protagonist
if (_sceneRoot != null)
{
_explorer = _sceneRoot.transform.Find("Explorer");
if (_explorer == null)
{
var expPrefab = Resources.Load<GameObject>("Explorer")
#if UNITY_EDITOR
?? UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/Characters/Human/Explorer/Explorer.prefab")
#endif
;
if (expPrefab != null)
{
var expInst = Instantiate(expPrefab, _sceneRoot.transform);
expInst.name = "Explorer";
expInst.transform.position = new Vector3(0f, 0f, -3.5f);
expInst.transform.rotation = Quaternion.LookRotation(Vector3.forward, Vector3.up);
_explorer = expInst.transform;
}
}
if (_explorer != null)
{
_explorerController = _explorer.GetComponent<HotUpdate.Gameplay.CampExplorerController>();
if (_explorerController == null)
_explorerController = _explorer.gameObject.AddComponent<HotUpdate.Gameplay.CampExplorerController>();
}
}
_sceneRoot.SetActive(isActiveAndEnabled);
if (isActiveAndEnabled)
{
Resize();
UpdateBuildingAnchors();
}
}
void BindBuildingBubbles()
{
var hud = transform.parent;
if (hud == null) return;
var group = hud.Find("Group_CampBuildings");
if (group == null) return;
_bubbleMainHouse = group.Find("Btn_Building_MainHouse") as RectTransform;
_bubbleShop = group.Find("Btn_Building_Shop") as RectTransform;
_bubblePets = group.Find("Btn_Building_Pets") as RectTransform;
_bubbleFountain = group.Find("Btn_Building_Fountain") as RectTransform;
_bubbleStall = group.Find("Btn_Building_Stall") as RectTransform;
}
void Update()
{
if (_camera != null && _image != null && _image.enabled)
{
HandleGroundClick();
}
}
void HandleGroundClick()
{
if (_camera == null || _explorerController == null) return;
bool isClick = false;
Vector2 screenPos = Vector2.zero;
if (Input.GetMouseButtonDown(0))
{
if (UnityEngine.EventSystems.EventSystem.current == null || !UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject())
{
isClick = true;
screenPos = Input.mousePosition;
}
}
else if (Input.touchCount > 0)
{
var touch = Input.GetTouch(0);
if (touch.phase == TouchPhase.Began)
{
if (UnityEngine.EventSystems.EventSystem.current == null || !UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject(touch.fingerId))
{
isClick = true;
screenPos = touch.position;
}
}
}
if (!isClick) return;
Ray ray = _camera.ScreenPointToRay(screenPos);
int layerMask = 1 << 29; // Layer 29 (HomeCamp)
if (Physics.Raycast(ray, out RaycastHit hit, 150f, layerMask))
{
// 优先检查是否点击中 3D 建筑本体
Transform cur = hit.collider.transform;
string matchedBuilding = null;
while (cur != null && cur != _sceneRoot.transform)
{
if (BuildingEntrances.ContainsKey(cur.name))
{
matchedBuilding = cur.name;
break;
}
cur = cur.parent;
}
if (!string.IsNullOrEmpty(matchedBuilding))
{
string actionKey = matchedBuilding;
if (matchedBuilding == "MainHouse") actionKey = "Camp";
else if (matchedBuilding == "Fountain") actionKey = "Rewards";
else if (matchedBuilding == "Stall") actionKey = "Daily";
else if (matchedBuilding == "HouseLeft") actionKey = "Shop";
else if (matchedBuilding == "HouseRight") actionKey = "Pets";
NavigateToBuilding(matchedBuilding, actionKey, () => _owner?.TriggerHomeAction(actionKey));
return;
}
// 点击普通地表或平整可走区域
if (hit.collider.name.IndexOf("Ground", StringComparison.OrdinalIgnoreCase) >= 0 || hit.point.y < 0.8f)
{
_currentNavBuilding = null;
_explorerController.MoveTo(hit.point);
}
}
}
void LateUpdate()
{
if (_camera != null && _image != null && _image.enabled)
{
Resize();
UpdateBuildingAnchors();
}
}
void Resize()
{
if (_camera == null || _image == null || Screen.width <= 0 || Screen.height <= 0) return;
float aspect = (float)Screen.width / Screen.height;
// Preserve the horizontal framing on long phones while showing more vertically.
_camera.fieldOfView = 2f * Mathf.Atan(Mathf.Tan(25f * Mathf.Deg2Rad) * Mathf.Max(1f, ReferenceAspect / aspect)) * Mathf.Rad2Deg;
// Dynamically scale and frame backdrop to avoid gaps on ultra-tall or ultra-wide displays
if (_backdrop != null)
{
const float dist = 75f;
float frustumHalfH = dist * Mathf.Tan(_camera.fieldOfView * Mathf.Deg2Rad * 0.5f);
float frustumHalfW = frustumHalfH * aspect;
float bdW = Mathf.Max(72f, frustumHalfW * 2f * 1.15f);
float bdH = Mathf.Max(56f, frustumHalfH * 1.35f);
float topHeight = frustumHalfH * 1.08f;
_backdrop.localScale = new Vector3(bdW, bdH, 1f);
_backdrop.position = _camera.transform.position + _camera.transform.forward * dist + _camera.transform.up * (topHeight - bdH * 0.5f);
_backdrop.rotation = _camera.transform.rotation;
}
int width = Mathf.Min(Screen.width, 720);
int height = Mathf.Max(1, Mathf.RoundToInt(width / aspect));
if (_texture == null || _texture.width != width || _texture.height != height)
{
_camera.targetTexture = null;
if (_texture != null) { _texture.Release(); Destroy(_texture); }
_texture = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32) { name = "HomeCampPreview" };
_texture.Create();
_camera.targetTexture = _texture;
_image.texture = _texture;
}
_camera.enabled = true;
_image.enabled = true;
}
void UpdateBuildingAnchors()
{
if (_camera == null) return;
var hud = transform.parent as RectTransform;
if (hud == null) return;
Vector2 canvasSize = hud.rect.size;
if (canvasSize.x <= 0 || canvasSize.y <= 0) return;
UpdateAnchor(_bubbleMainHouse, _mainHouse, new Vector3(0, 6.7f, 7.2f), canvasSize, 0);
UpdateAnchor(_bubbleShop, _houseLeft, new Vector3(-6.8f, 4.0f, 3.6f), canvasSize, 1);
UpdateAnchor(_bubblePets, _houseRight, new Vector3(7.5f, 4.2f, 4.0f), canvasSize, 2);
UpdateAnchor(_bubbleFountain, _fountain, new Vector3(0, 2.5f, 0.8f), canvasSize, 3);
UpdateAnchor(_bubbleStall, _stall, new Vector3(4.5f, 2.6f, 2.8f), canvasSize, 4);
}
void UpdateAnchor(RectTransform bubble, Transform targetTf, Vector3 anchorPos, Vector2 canvasSize, int bobIndex)
{
if (bubble == null) return;
Vector3 worldPos = targetTf != null ? targetTf.position : anchorPos;
worldPos.y = anchorPos.y;
worldPos.x = anchorPos.x;
worldPos.z = anchorPos.z;
Vector3 vp = _camera.WorldToViewportPoint(worldPos);
if (vp.z <= 0 || vp.x < -0.2f || vp.x > 1.2f || vp.y < -0.2f || vp.y > 1.2f)
{
if (bubble.gameObject.activeSelf) bubble.gameObject.SetActive(false);
return;
}
if (!bubble.gameObject.activeSelf) bubble.gameObject.SetActive(true);
float x = (vp.x - 0.5f) * canvasSize.x;
float y = (vp.y - 0.5f) * canvasSize.y;
float bob = Mathf.Sin(Time.time * 2.5f + bobIndex * 1.2f) * 2.5f;
y += bob;
float halfW = canvasSize.x * 0.5f;
float halfH = canvasSize.y * 0.5f;
float bubbleHalfW = bubble.rect.width * 0.5f;
x = Mathf.Clamp(x, -halfW + bubbleHalfW + 15f, halfW - bubbleHalfW - 15f);
y = Mathf.Clamp(y, -halfH + 280f, halfH - 180f);
bubble.anchoredPosition = new Vector2(x, y);
}
void OnDestroy()
{
_destroyed = true;
if (_image != null) _image.texture = null;
if (_camera != null) _camera.targetTexture = null;
if (_texture != null) { _texture.Release(); Destroy(_texture); }
if (_handle != null && !_loading && YooAsset.YooAssets.IsInitialized && _handle.IsValid && _handle.IsDone)
{
ReleaseScene(_handle);
_handle = null;
}
#if UNITY_EDITOR
else if (_handle == null && !_loading)
{
var scene = UnityEngine.SceneManagement.SceneManager.GetSceneByPath(ScenePath);
if (scene.IsValid() && scene.isLoaded) UnityEngine.SceneManagement.SceneManager.UnloadSceneAsync(scene);
}
#endif
}
}
}