using System.Collections; using AibisDream.Utility; using TMPro; using UnityEngine; using UnityEngine.Localization.Components; using UnityEngine.UI; namespace AibisDream.MiniGame.Language { /// /// Drives the full-frame Huoshan expression screen while keeping readable text in TMP. /// The sprite sheet uses the original 384x216 Aseprite timing and the existing world-space /// Canvas remains the source of truth for interaction and dynamic language particles. /// public sealed class ExpressionScreenPresentationController : MonoBehaviour { public enum ScreenState { Reset, Deep, Panel, Output, Floating, Click, FocusHidden } private const int DeepFirst = 0; private const int DeepLast = 14; private const int UiFlashFirst = 15; private const int UiFlashLast = 29; private const int StatusRevealFrame = 21; private const int RingFirst = 30; private const int RingLast = 40; private const int OutputFirst = 41; private const int OutputLast = 47; private const int OutputTextRevealFrame = 45; private const int FloatingFrame = 48; private const int ClickFirst = 49; private const int ClickLast = 52; private const string ObjectiveLocalizationKey = "huoshan_expression_objective"; private const string OutputLocalizationKey = "huoshan_expression_output"; [Header("Frame Animation")] [SerializeField] private SpriteRenderer screenRenderer; [SerializeField] private Sprite[] frames = new Sprite[53]; [SerializeField] private Vector3 screenLocalPosition = new Vector3(0.68f, -8.95f, 0f); [SerializeField] private Vector3 screenLocalScale = new Vector3(1.1f, 1.1f, 1f); [Header("Existing TMP / Interaction")] [SerializeField] private TMP_Text integrationProgressText; [SerializeField] private TMP_Text interferenceCountText; [SerializeField] private Button outputButton; [SerializeField] private TMP_FontAsset screenFont; [Header("Status TMP (screen-space world units)")] [SerializeField] private Vector2 integrationStatusPosition = new Vector2(-8.75f, 4.15f); [SerializeField] private Vector2 interferenceStatusPosition = new Vector2(-8.75f, 3.08f); [SerializeField] private Vector2 statusPanelSize = new Vector2(4.55f, 0.6f); [SerializeField] private Vector4 statusTextMargins = new Vector4(0.16f, 0.05f, 0.08f, 0.05f); [SerializeField] private float statusFontSize = 0.26f; [SerializeField] private Color screenTextColor = new Color(0.78f, 0.91f, 1f, 1f); [Header("Output Button Hit Area")] [SerializeField] private Vector2 outputButtonPosition = new Vector2(4.86f, -0.065f); [SerializeField] private Vector2 outputButtonSize = new Vector2(2.72f, 1.13f); [Header("Objective TMP (384x216 reference)")] [SerializeField] private Vector2 objectivePosition = new Vector2(-8.18f, -4.08f); [SerializeField] private Vector2 objectiveSize = new Vector2(7.4f, 0.95f); [SerializeField] private float objectiveFontSize = 0.28f; [SerializeField] private Color objectiveColor = new Color(0.78f, 0.91f, 1f, 1f); private int playbackGeneration; private TMP_Text objectiveText; private TMP_Text outputText; private LocalizeStringEvent objectiveLocalization; private LocalizeStringEvent outputLocalization; private Material objectiveMaterial; private ScreenState state = ScreenState.Reset; public ScreenState State => state; private void Awake() { ResolveSceneReferences(); ConfigureExistingUiAsTextOnly(); CreateObjectiveText(); BindStaticLocalization(); ResetImmediate(); } private void ResolveSceneReferences() { if (screenRenderer == null) { SpriteRenderer[] renderers = GetComponentsInChildren(true); foreach (SpriteRenderer candidate in renderers) { if (candidate != null && candidate.gameObject.name == "火山释放log界面") { screenRenderer = candidate; break; } } } if (outputButton != null) { outputText = outputButton.GetComponentInChildren(true); } if (screenRenderer != null) { screenRenderer.transform.localPosition = screenLocalPosition; screenRenderer.transform.localScale = screenLocalScale; } } private void ConfigureExistingUiAsTextOnly() { MakeParentImageTransparent(integrationProgressText); MakeParentImageTransparent(interferenceCountText); ConfigureStatusText(integrationProgressText, integrationStatusPosition); ConfigureStatusText(interferenceCountText, interferenceStatusPosition); ConfigureOutputText(); if (outputButton != null && outputButton.targetGraphic != null) { Color color = outputButton.targetGraphic.color; color.a = 0f; outputButton.targetGraphic.color = color; outputButton.targetGraphic.raycastTarget = true; } } private void ConfigureStatusText(TMP_Text text, Vector2 panelPosition) { if (text == null) return; if (screenFont != null) text.font = screenFont; text.fontSize = statusFontSize; text.fontStyle = FontStyles.Normal; text.color = screenTextColor; text.alignment = TextAlignmentOptions.MidlineLeft; text.enableWordWrapping = false; text.overflowMode = TextOverflowModes.Truncate; text.margin = statusTextMargins; text.raycastTarget = false; if (text.transform.parent is RectTransform panelRect) { panelRect.anchorMin = new Vector2(0.5f, 0.5f); panelRect.anchorMax = new Vector2(0.5f, 0.5f); panelRect.pivot = new Vector2(0f, 0.5f); panelRect.anchoredPosition = panelPosition; panelRect.sizeDelta = statusPanelSize; } RectTransform textRect = text.rectTransform; textRect.anchorMin = Vector2.zero; textRect.anchorMax = Vector2.one; textRect.pivot = new Vector2(0.5f, 0.5f); textRect.anchoredPosition = Vector2.zero; textRect.sizeDelta = Vector2.zero; } private void ConfigureOutputText() { if (outputButton == null) return; RectTransform buttonRect = outputButton.transform as RectTransform; if (buttonRect != null) { buttonRect.anchorMin = new Vector2(0.5f, 0.5f); buttonRect.anchorMax = new Vector2(0.5f, 0.5f); buttonRect.pivot = new Vector2(0f, 0.5f); buttonRect.anchoredPosition = outputButtonPosition; buttonRect.sizeDelta = outputButtonSize; } if (outputText == null) return; if (screenFont != null) outputText.font = screenFont; outputText.fontStyle = FontStyles.Normal; outputText.color = screenTextColor; outputText.alignment = TextAlignmentOptions.Center; outputText.enableWordWrapping = false; outputText.overflowMode = TextOverflowModes.Truncate; RectTransform textRect = outputText.rectTransform; textRect.anchorMin = Vector2.zero; textRect.anchorMax = Vector2.one; textRect.pivot = new Vector2(0.5f, 0.5f); textRect.anchoredPosition = Vector2.zero; textRect.sizeDelta = Vector2.zero; } private static void MakeParentImageTransparent(TMP_Text text) { if (text == null || text.transform.parent == null) return; Image image = text.transform.parent.GetComponent(); if (image == null) return; Color color = image.color; color.a = 0f; image.color = color; image.raycastTarget = false; } private void CreateObjectiveText() { Canvas canvas = integrationProgressText != null ? integrationProgressText.GetComponentInParent() : GetComponentInChildren(true); if (canvas == null) { Debug.LogError("[ExpressionScreenPresentation] ExpressView 下未找到 World Space Canvas,任务描述无法创建。"); return; } Transform existing = canvas.transform.Find("Expression Objective (TMP)"); if (existing != null) { objectiveText = existing.GetComponent(); return; } var objectiveObject = new GameObject( "Expression Objective (TMP)", typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI)); objectiveObject.layer = canvas.gameObject.layer; RectTransform rect = objectiveObject.GetComponent(); rect.SetParent(canvas.transform, false); rect.anchorMin = new Vector2(0.5f, 0.5f); rect.anchorMax = new Vector2(0.5f, 0.5f); rect.pivot = new Vector2(0f, 0.5f); rect.anchoredPosition = objectivePosition; rect.sizeDelta = objectiveSize; rect.SetAsFirstSibling(); var text = objectiveObject.GetComponent(); text.font = screenFont; if (integrationProgressText != null && integrationProgressText.fontSharedMaterial != null) { objectiveMaterial = new Material(integrationProgressText.fontSharedMaterial) { name = "Huoshan Expression Objective TMP (Runtime)" }; text.fontSharedMaterial = objectiveMaterial; } text.fontSize = objectiveFontSize; text.fontStyle = FontStyles.Normal; text.color = objectiveColor; text.alignment = TextAlignmentOptions.MidlineLeft; text.enableWordWrapping = true; text.overflowMode = TextOverflowModes.Truncate; text.raycastTarget = false; objectiveText = text; } private void BindStaticLocalization() { objectiveLocalization = BindLocalizedText(objectiveText, ObjectiveLocalizationKey); outputLocalization = BindLocalizedText(outputText, OutputLocalizationKey); } private static LocalizeStringEvent BindLocalizedText(TMP_Text text, string entryKey) { if (text == null) return null; LocalizeStringEvent localize = text.GetComponent(); if (localize == null) localize = text.gameObject.AddComponent(); localize.StringReference.SetReference(ConstRef.UITextTable, entryKey); localize.OnUpdateString.RemoveAllListeners(); localize.OnUpdateString.AddListener(value => text.text = value); localize.StringReference.RefreshString(); return localize; } public IEnumerator PlayDeepAndWait() { int generation = BeginPlayback(); SetOverlayVisibility(false, false, false); yield return PlayRangeAndWait(DeepFirst, DeepLast, generation); if (generation != playbackGeneration) yield break; state = ScreenState.Deep; } public IEnumerator PlayPanelAndWait() { int generation = BeginPlayback(); yield return PlayRangeAndWait(UiFlashFirst, UiFlashLast, generation, StatusRevealFrame, ShowStatusAndObjective); if (generation != playbackGeneration) yield break; yield return PlayRangeAndWait(RingFirst, RingLast, generation); if (generation != playbackGeneration) yield break; ShowStatusAndObjective(); state = ScreenState.Panel; } public IEnumerator PlayOutputAndWait() { int generation = BeginPlayback(); SetOutputTextVisible(false); yield return PlayRangeAndWait( OutputFirst, OutputLast, generation, OutputTextRevealFrame, () => SetOutputTextVisible(true)); if (generation != playbackGeneration) yield break; SetFrame(FloatingFrame); SetOutputTextVisible(true); state = ScreenState.Floating; } public IEnumerator PlayClickAndWait() { int generation = BeginPlayback(); SetOutputTextVisible(true); yield return PlayRangeAndWait(ClickFirst, ClickLast, generation); if (generation != playbackGeneration) yield break; state = ScreenState.Click; } public void PrepareForNewRound() { StopPlayback(); SetFrame(DeepFirst); SetOverlayVisibility(false, false, false); state = ScreenState.Reset; } public void HideOverlayForFocus() { SetOverlayVisibility(false, false, false); state = ScreenState.FocusHidden; } public void ResetImmediate() { StopPlayback(); SetFrame(DeepFirst); SetOverlayVisibility(false, false, false); if (outputButton != null) { outputButton.interactable = false; outputButton.gameObject.SetActive(false); } state = ScreenState.Reset; } private int BeginPlayback() { StopPlayback(); return playbackGeneration; } private IEnumerator PlayRangeAndWait( int firstFrame, int lastFrame, int generation, int revealFrame = -1, System.Action revealAction = null) { for (int frameIndex = firstFrame; frameIndex <= lastFrame; frameIndex++) { if (generation != playbackGeneration) yield break; SetFrame(frameIndex); if (frameIndex == revealFrame) revealAction?.Invoke(); yield return new WaitForSeconds(GetFrameDuration(frameIndex)); } } private void SetFrame(int frameIndex) { if (screenRenderer == null || frames == null || frameIndex < 0 || frameIndex >= frames.Length || frames[frameIndex] == null) { return; } screenRenderer.gameObject.SetActive(true); screenRenderer.enabled = true; screenRenderer.sprite = frames[frameIndex]; } private static float GetFrameDuration(int frameIndex) { switch (frameIndex) { case 0: case 29: case 47: case 48: return 0.3f; case 16: case 17: case 21: case 25: case 49: case 50: case 51: case 52: return 0.05f; default: return 0.1f; } } private void ShowStatusAndObjective() { SetOverlayVisibility(true, true, false); } private void SetOverlayVisibility(bool statusVisible, bool objectiveVisible, bool outputVisible) { SetStatusTextVisible(integrationProgressText, statusVisible); SetStatusTextVisible(interferenceCountText, statusVisible); if (objectiveText != null) objectiveText.gameObject.SetActive(objectiveVisible); SetOutputTextVisible(outputVisible); } private static void SetStatusTextVisible(TMP_Text text, bool visible) { if (text == null) return; if (visible) { var canvasGroup = text.GetComponentInParent(); if (canvasGroup != null) canvasGroup.alpha = 1f; } if (text.transform.parent != null) text.transform.parent.gameObject.SetActive(visible); text.gameObject.SetActive(visible); } private void SetOutputTextVisible(bool visible) { if (outputText != null) outputText.gameObject.SetActive(visible); } private void StopPlayback() { playbackGeneration++; } private void OnDestroy() { if (objectiveLocalization != null) objectiveLocalization.OnUpdateString.RemoveAllListeners(); if (outputLocalization != null) outputLocalization.OnUpdateString.RemoveAllListeners(); if (objectiveMaterial != null) Destroy(objectiveMaterial); } } }