Files
aibis-dream/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionScreenPresentationController.cs
T

493 lines
18 KiB
C#

using System.Collections;
using AibisDream.Utility;
using TMPro;
using UnityEngine;
using UnityEngine.Localization.Components;
using UnityEngine.UI;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 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.
/// </summary>
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<SpriteRenderer>(true);
foreach (SpriteRenderer candidate in renderers)
{
if (candidate != null && candidate.gameObject.name == "火山释放log界面")
{
screenRenderer = candidate;
break;
}
}
}
if (outputButton != null)
{
outputText = outputButton.GetComponentInChildren<TMP_Text>(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<Image>();
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<Canvas>()
: GetComponentInChildren<Canvas>(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<TMP_Text>();
return;
}
var objectiveObject = new GameObject(
"Expression Objective (TMP)",
typeof(RectTransform),
typeof(CanvasRenderer),
typeof(TextMeshProUGUI));
objectiveObject.layer = canvas.gameObject.layer;
RectTransform rect = objectiveObject.GetComponent<RectTransform>();
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<TextMeshProUGUI>();
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<LocalizeStringEvent>();
if (localize == null)
localize = text.gameObject.AddComponent<LocalizeStringEvent>();
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<CanvasGroup>();
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);
}
}
}