493 lines
18 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|