Runtime UI under Scripts/UI: a UIStateController owning menu/dialogue state, a PlayerControlLock so opening a panel suppresses world input, panel visibility helpers, and the menu input binding. Views: the dialogue stream presenter and its options presenter and message row, the portrait HUD, and the player menu with character, journal, and tab-bar views. Skills gains SkillsPanelView and SkillRowView, which is why the Skills asmdef now references UnityEngine.UI. Prefabs/UI holds the authored canvases (dialogue stream, HUD, player menu) and their rows. UILayerBootstrap remains a hand-added runtime fallback for scenes where those prefabs are not instantiated; it deliberately does not auto-spawn. Editor/UILayerSetup.cs is the one-shot menu that builds and wires those prefabs into a scene, with UILayerSetupAutoRun reminding after a script reload. Art/UI/Placeholder carries the placeholder portrait the HUD falls back to. Co-Authored-By: Claude Opus 5 <[email protected]>
647 lines
29 KiB
C#
647 lines
29 KiB
C#
using NightclubArcadia.Cinematics;
|
|
using NightclubArcadia.Dialogue;
|
|
using NightclubArcadia.Skills;
|
|
using NightclubArcadia.UI.Dialogue;
|
|
using NightclubArcadia.UI.HUD;
|
|
using NightclubArcadia.UI.Menu;
|
|
using StarterAssets;
|
|
using TMPro;
|
|
using Unity.Cinemachine;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using Yarn.Unity;
|
|
|
|
namespace NightclubArcadia.UI
|
|
{
|
|
/// <summary>
|
|
/// Builds the rudimentary UI layer at runtime, for scenes where the UI
|
|
/// prefabs have not been instantiated. It is a fallback only: the authored
|
|
/// path is Tools → Nightclub Arcadia → Set Up UI Layer, which produces the
|
|
/// prefabs under Assets/Prefabs/UI and wires them into the scene.
|
|
///
|
|
/// It does NOT auto-spawn. It used to, which meant that once the scene had
|
|
/// been wired properly it would still create a second "UI System" object
|
|
/// carrying a duplicate UIStateController — logging a Debug.LogError every
|
|
/// play — and would reflection-overwrite DialogueCameraFraming's serialised
|
|
/// dialogueScreenX, making that value impossible to tune in the Inspector.
|
|
/// Add this component to a scene by hand if you want the fallback.
|
|
/// </summary>
|
|
[DefaultExecutionOrder(-100)]
|
|
public sealed class UILayerBootstrap : MonoBehaviour
|
|
{
|
|
[SerializeField] bool buildIfMissing = true;
|
|
|
|
void Awake()
|
|
{
|
|
EnsureFoundation();
|
|
|
|
if (!buildIfMissing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (FindFirstObjectByType<DialogueStreamPresenter>() == null)
|
|
{
|
|
BuildDialogueStream();
|
|
}
|
|
|
|
if (FindFirstObjectByType<PortraitHudView>() == null)
|
|
{
|
|
BuildHud();
|
|
}
|
|
|
|
if (FindFirstObjectByType<PlayerMenuController>() == null)
|
|
{
|
|
BuildMenu();
|
|
}
|
|
|
|
EnsureCameraFraming();
|
|
WireDialogueRunner();
|
|
HidePackageDialogueCanvas();
|
|
}
|
|
|
|
void EnsureFoundation()
|
|
{
|
|
if (UIStateController.Instance == null && FindFirstObjectByType<UIStateController>() == null)
|
|
{
|
|
var go = GameObject.Find("UI System") ?? new GameObject("UI System");
|
|
go.AddComponent<UIStateController>();
|
|
}
|
|
|
|
var player = GameObject.Find("PlayerArmature");
|
|
if (player == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (player.GetComponent<PlayerControlLock>() == null)
|
|
{
|
|
player.AddComponent<PlayerControlLock>();
|
|
}
|
|
|
|
if (player.GetComponent<PlayerMenuInput>() == null)
|
|
{
|
|
player.AddComponent<PlayerMenuInput>();
|
|
}
|
|
|
|
var inputs = player.GetComponent<StarterAssetsInputs>();
|
|
if (inputs != null)
|
|
{
|
|
inputs.cursorLocked = false;
|
|
Cursor.lockState = CursorLockMode.None;
|
|
Cursor.visible = true;
|
|
}
|
|
|
|
var transition = FindFirstObjectByType<DialogueTransitionController>();
|
|
if (transition != null)
|
|
{
|
|
var soLock = player.GetComponent<PlayerControlLock>();
|
|
// Assign via reflection-free serialized path: public setter not available,
|
|
// so use SendMessage-style field via SerializedObject is editor-only.
|
|
// Runtime: DialogueTransitionController finds lock from playerArmature in Awake.
|
|
var field = typeof(DialogueTransitionController).GetField(
|
|
"controlLock",
|
|
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
|
field?.SetValue(transition, soLock);
|
|
}
|
|
}
|
|
|
|
void BuildDialogueStream()
|
|
{
|
|
var canvasGo = CreateCanvas("DialogueStreamCanvas", 5);
|
|
var canvasGroup = canvasGo.AddComponent<CanvasGroup>();
|
|
canvasGroup.alpha = 0f;
|
|
canvasGroup.interactable = false;
|
|
canvasGroup.blocksRaycasts = false;
|
|
|
|
var panel = new GameObject("Panel", typeof(RectTransform), typeof(Image));
|
|
panel.transform.SetParent(canvasGo.transform, false);
|
|
var panelRt = panel.GetComponent<RectTransform>();
|
|
panelRt.anchorMin = new Vector2(0.66f, 0f);
|
|
panelRt.anchorMax = Vector2.one;
|
|
panelRt.offsetMin = Vector2.zero;
|
|
panelRt.offsetMax = Vector2.zero;
|
|
panel.GetComponent<Image>().color = new Color(0.05f, 0.05f, 0.08f, 0.92f);
|
|
|
|
var scrollGo = new GameObject("Scroll", typeof(RectTransform), typeof(ScrollRect), typeof(Image));
|
|
scrollGo.transform.SetParent(panel.transform, false);
|
|
Stretch(scrollGo.GetComponent<RectTransform>(), 8f);
|
|
scrollGo.GetComponent<Image>().color = new Color(0f, 0f, 0f, 0.01f);
|
|
|
|
var viewport = new GameObject("Viewport", typeof(RectTransform), typeof(RectMask2D), typeof(Image));
|
|
viewport.transform.SetParent(scrollGo.transform, false);
|
|
Stretch(viewport.GetComponent<RectTransform>());
|
|
viewport.GetComponent<Image>().color = new Color(1f, 1f, 1f, 0.01f);
|
|
|
|
var content = new GameObject("Content", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
|
|
content.transform.SetParent(viewport.transform, false);
|
|
var contentRt = content.GetComponent<RectTransform>();
|
|
contentRt.anchorMin = new Vector2(0f, 0f);
|
|
contentRt.anchorMax = new Vector2(1f, 0f);
|
|
contentRt.pivot = new Vector2(0.5f, 0f);
|
|
var vlg = content.GetComponent<VerticalLayoutGroup>();
|
|
vlg.childAlignment = TextAnchor.LowerCenter;
|
|
vlg.childControlHeight = true;
|
|
vlg.childControlWidth = true;
|
|
vlg.childForceExpandHeight = false;
|
|
vlg.childForceExpandWidth = true;
|
|
vlg.spacing = 6f;
|
|
content.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
|
|
var messages = new GameObject("Messages", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
|
|
messages.transform.SetParent(content.transform, false);
|
|
ConfigureVertical(messages);
|
|
|
|
var options = new GameObject("Options", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter), typeof(CanvasGroup));
|
|
options.transform.SetParent(content.transform, false);
|
|
ConfigureVertical(options);
|
|
var optionsGroup = options.GetComponent<CanvasGroup>();
|
|
optionsGroup.alpha = 0f;
|
|
optionsGroup.interactable = false;
|
|
optionsGroup.blocksRaycasts = false;
|
|
|
|
var scroll = scrollGo.GetComponent<ScrollRect>();
|
|
scroll.content = contentRt;
|
|
scroll.viewport = viewport.GetComponent<RectTransform>();
|
|
scroll.horizontal = false;
|
|
scroll.vertical = true;
|
|
|
|
var templates = new GameObject("UITemplates");
|
|
templates.transform.SetParent(transform, false);
|
|
templates.SetActive(false);
|
|
|
|
var rowPrefab = BuildMessageRowPrefab(templates.transform);
|
|
|
|
var stream = canvasGo.AddComponent<DialogueStreamPresenter>();
|
|
SetPrivate(stream, "messageRowPrefab", rowPrefab);
|
|
SetPrivate(stream, "messageContainer", messages.GetComponent<RectTransform>());
|
|
SetPrivate(stream, "scrollRect", scroll);
|
|
|
|
OptionItem yarnOption = null;
|
|
#if UNITY_EDITOR
|
|
yarnOption = UnityEditor.AssetDatabase.LoadAssetAtPath<OptionItem>(
|
|
"Packages/dev.yarnspinner.unity/Prefabs/Option Item.prefab");
|
|
#endif
|
|
if (yarnOption == null)
|
|
{
|
|
yarnOption = BuildFallbackOptionItem(templates.transform);
|
|
}
|
|
|
|
var optionsPresenter = canvasGo.AddComponent<DialogueStreamOptionsPresenter>();
|
|
SetPrivate(optionsPresenter, "optionViewPrefab", yarnOption);
|
|
SetPrivate(optionsPresenter, "optionsContainer", options.transform);
|
|
SetPrivate(optionsPresenter, "canvasGroup", optionsGroup);
|
|
|
|
var transition = FindFirstObjectByType<DialogueTransitionController>();
|
|
if (transition != null)
|
|
{
|
|
SetPrivate(transition, "dialogueUI", canvasGo);
|
|
}
|
|
|
|
foreach (var npc in FindObjectsByType<NPCStandIn>(FindObjectsInactive.Include, FindObjectsSortMode.None))
|
|
{
|
|
npc.dialogueUI = canvasGo;
|
|
}
|
|
}
|
|
|
|
DialogueMessageRow BuildMessageRowPrefab(Transform parent)
|
|
{
|
|
var root = new GameObject("DialogueMessageRow", typeof(RectTransform), typeof(DialogueMessageRow), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
|
|
root.transform.SetParent(parent, false);
|
|
var layout = root.GetComponent<VerticalLayoutGroup>();
|
|
layout.padding = new RectOffset(12, 12, 8, 8);
|
|
layout.spacing = 4f;
|
|
layout.childControlHeight = true;
|
|
layout.childControlWidth = true;
|
|
layout.childForceExpandWidth = true;
|
|
layout.childForceExpandHeight = false;
|
|
root.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
|
|
var speaker = CreateTmp("Speaker", root.transform, 22f, FontStyles.Bold);
|
|
var body = CreateTmp("Body", root.transform, 28f, FontStyles.Normal);
|
|
var row = root.GetComponent<DialogueMessageRow>();
|
|
SetPrivate(row, "speakerText", speaker);
|
|
SetPrivate(row, "bodyText", body);
|
|
SetPrivate(row, "speakerLabel", speaker.gameObject);
|
|
return row;
|
|
}
|
|
|
|
OptionItem BuildFallbackOptionItem(Transform parent)
|
|
{
|
|
var go = new GameObject("OptionItem", typeof(RectTransform), typeof(Image), typeof(OptionItem));
|
|
go.transform.SetParent(parent, false);
|
|
go.GetComponent<Image>().color = new Color(0.15f, 0.15f, 0.2f, 1f);
|
|
var text = CreateTmp("Text", go.transform, 24f, FontStyles.Normal);
|
|
Stretch(text.rectTransform);
|
|
SetPrivate(go.GetComponent<OptionItem>(), "text", text);
|
|
return go.GetComponent<OptionItem>();
|
|
}
|
|
|
|
void BuildHud()
|
|
{
|
|
var canvasGo = CreateCanvas("HudCanvas", 10);
|
|
var panel = new GameObject("PortraitPanel", typeof(RectTransform), typeof(CanvasGroup), typeof(Image), typeof(Button));
|
|
panel.transform.SetParent(canvasGo.transform, false);
|
|
var rt = panel.GetComponent<RectTransform>();
|
|
rt.anchorMin = new Vector2(1f, 0f);
|
|
rt.anchorMax = new Vector2(1f, 0f);
|
|
rt.pivot = new Vector2(1f, 0f);
|
|
rt.sizeDelta = new Vector2(220f, 280f);
|
|
rt.anchoredPosition = new Vector2(-24f, 24f);
|
|
panel.GetComponent<Image>().color = new Color(0.15f, 0.15f, 0.18f, 0.95f);
|
|
|
|
// Lives in Assets/Art/UI/Placeholder/Resources so this resolves in
|
|
// builds too — the AssetDatabase fallback below is editor-only.
|
|
var sprite = Resources.Load<Sprite>("portrait_placeholder");
|
|
#if UNITY_EDITOR
|
|
if (sprite == null)
|
|
{
|
|
sprite = UnityEditor.AssetDatabase.LoadAssetAtPath<Sprite>(
|
|
"Assets/Art/UI/Placeholder/Resources/portrait_placeholder.png");
|
|
}
|
|
#endif
|
|
if (sprite != null)
|
|
{
|
|
panel.GetComponent<Image>().sprite = sprite;
|
|
panel.GetComponent<Image>().preserveAspect = true;
|
|
}
|
|
|
|
var hint = CreateTmp("TabHint", panel.transform, 18f, FontStyles.Normal);
|
|
hint.text = "Tab";
|
|
hint.alignment = TextAlignmentOptions.Center;
|
|
var hintRt = hint.rectTransform;
|
|
hintRt.anchorMin = new Vector2(0f, 0f);
|
|
hintRt.anchorMax = new Vector2(1f, 0f);
|
|
hintRt.pivot = new Vector2(0.5f, 0f);
|
|
hintRt.sizeDelta = new Vector2(0f, 28f);
|
|
hintRt.anchoredPosition = new Vector2(0f, 4f);
|
|
|
|
var view = canvasGo.AddComponent<PortraitHudView>();
|
|
SetPrivate(view, "panelRoot", panel);
|
|
}
|
|
|
|
void BuildMenu()
|
|
{
|
|
var canvasGo = CreateCanvas("PlayerMenuCanvas", 20);
|
|
var rootGroup = canvasGo.AddComponent<CanvasGroup>();
|
|
rootGroup.alpha = 0f;
|
|
rootGroup.interactable = false;
|
|
rootGroup.blocksRaycasts = false;
|
|
|
|
var dimmer = new GameObject("Dimmer", typeof(RectTransform), typeof(Image));
|
|
dimmer.transform.SetParent(canvasGo.transform, false);
|
|
Stretch(dimmer.GetComponent<RectTransform>());
|
|
dimmer.GetComponent<Image>().color = new Color(0f, 0f, 0f, 0.6f);
|
|
|
|
var body = new GameObject("MenuBody", typeof(RectTransform), typeof(Image));
|
|
body.transform.SetParent(canvasGo.transform, false);
|
|
var bodyRt = body.GetComponent<RectTransform>();
|
|
bodyRt.anchorMin = new Vector2(0.15f, 0.1f);
|
|
bodyRt.anchorMax = new Vector2(0.85f, 0.9f);
|
|
bodyRt.offsetMin = Vector2.zero;
|
|
bodyRt.offsetMax = Vector2.zero;
|
|
body.GetComponent<Image>().color = new Color(0.08f, 0.08f, 0.1f, 0.98f);
|
|
|
|
var tabBarGo = new GameObject("TabBar", typeof(RectTransform), typeof(HorizontalLayoutGroup), typeof(MenuTabBar));
|
|
tabBarGo.transform.SetParent(body.transform, false);
|
|
var tabBarRt = tabBarGo.GetComponent<RectTransform>();
|
|
tabBarRt.anchorMin = new Vector2(0f, 1f);
|
|
tabBarRt.anchorMax = new Vector2(1f, 1f);
|
|
tabBarRt.pivot = new Vector2(0.5f, 1f);
|
|
tabBarRt.sizeDelta = new Vector2(0f, 56f);
|
|
var tabHlg = tabBarGo.GetComponent<HorizontalLayoutGroup>();
|
|
tabHlg.spacing = 8f;
|
|
tabHlg.padding = new RectOffset(12, 12, 8, 8);
|
|
// childControl* default to false on a script-added layout group.
|
|
tabHlg.childControlWidth = true;
|
|
tabHlg.childControlHeight = true;
|
|
tabHlg.childForceExpandWidth = true;
|
|
tabHlg.childForceExpandHeight = true;
|
|
|
|
var contentArea = new GameObject("ContentArea", typeof(RectTransform));
|
|
contentArea.transform.SetParent(body.transform, false);
|
|
var contentRt = contentArea.GetComponent<RectTransform>();
|
|
contentRt.anchorMin = Vector2.zero;
|
|
contentRt.anchorMax = Vector2.one;
|
|
contentRt.offsetMin = new Vector2(16f, 16f);
|
|
contentRt.offsetMax = new Vector2(-16f, -64f);
|
|
|
|
var characterPanel = CreateTabPanel("CharacterPanel", contentArea.transform);
|
|
var skillsPanel = CreateTabPanel("SkillsPanel", contentArea.transform);
|
|
var journalPanel = CreateTabPanel("JournalPanel", contentArea.transform);
|
|
|
|
// Character
|
|
var charVlg = characterPanel.AddComponent<VerticalLayoutGroup>();
|
|
charVlg.spacing = 12f;
|
|
charVlg.padding = new RectOffset(8, 8, 8, 8);
|
|
charVlg.childControlWidth = true;
|
|
charVlg.childControlHeight = true;
|
|
charVlg.childForceExpandWidth = true;
|
|
var charView = characterPanel.AddComponent<CharacterPanelView>();
|
|
var nameLabel = CreateTmp("Name", characterPanel.transform, 36f, FontStyles.Bold);
|
|
var aliasLabel = CreateTmp("Alias", characterPanel.transform, 28f, FontStyles.Italic);
|
|
var invLabel = CreateTmp("Inventory", characterPanel.transform, 24f, FontStyles.Normal);
|
|
SetPrivate(charView, "nameLabel", nameLabel);
|
|
SetPrivate(charView, "aliasLabel", aliasLabel);
|
|
SetPrivate(charView, "inventoryPlaceholder", invLabel);
|
|
|
|
// Skills
|
|
var skillsContent = new GameObject("Content", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
|
|
skillsContent.transform.SetParent(skillsPanel.transform, false);
|
|
Stretch(skillsContent.GetComponent<RectTransform>());
|
|
ConfigureVertical(skillsContent);
|
|
skillsContent.GetComponent<RectTransform>().anchorMin = new Vector2(0f, 1f);
|
|
skillsContent.GetComponent<RectTransform>().anchorMax = new Vector2(1f, 1f);
|
|
skillsContent.GetComponent<RectTransform>().pivot = new Vector2(0.5f, 1f);
|
|
|
|
var axisHeader = CreateTmp("AxisHeaderTemplate", skillsContent.transform, 20f, FontStyles.Bold);
|
|
axisHeader.gameObject.SetActive(false);
|
|
var templates = GameObject.Find("UI System")?.transform.Find("UITemplates")
|
|
?? new GameObject("UITemplates").transform;
|
|
if (templates.parent == null && transform != null)
|
|
{
|
|
templates.SetParent(transform, false);
|
|
templates.gameObject.SetActive(false);
|
|
}
|
|
|
|
var rowPrefab = BuildSkillRowPrefab(templates);
|
|
var skillsView = skillsPanel.AddComponent<SkillsPanelView>();
|
|
SetPrivate(skillsView, "contentRoot", skillsContent.GetComponent<RectTransform>());
|
|
SetPrivate(skillsView, "rowPrefab", rowPrefab);
|
|
SetPrivate(skillsView, "axisHeaderPrefab", axisHeader);
|
|
SetPrivate(skillsView, "displayMaxRank", 6);
|
|
|
|
// Journal
|
|
var journalView = journalPanel.AddComponent<JournalPanelView>();
|
|
var journalLabel = CreateTmp("Placeholder", journalPanel.transform, 28f, FontStyles.Italic);
|
|
journalLabel.text = "Nothing recorded yet.";
|
|
journalLabel.alignment = TextAlignmentOptions.Center;
|
|
Stretch(journalLabel.rectTransform);
|
|
SetPrivate(journalView, "placeholderLabel", journalLabel);
|
|
|
|
var charBtn = CreateTabButton("Character", tabBarGo.transform);
|
|
var skillsBtn = CreateTabButton("Skills", tabBarGo.transform);
|
|
var journalBtn = CreateTabButton("Journal", tabBarGo.transform);
|
|
|
|
var tabBar = tabBarGo.GetComponent<MenuTabBar>();
|
|
var tabs = new MenuTabBar.TabEntry[3];
|
|
tabs[0] = new MenuTabBar.TabEntry
|
|
{
|
|
button = charBtn,
|
|
panel = characterPanel.GetComponent<CanvasGroup>(),
|
|
onShown = new UnityEngine.Events.UnityEvent(),
|
|
};
|
|
// Panels toggle via CanvasGroup, never SetActive, so OnEnable fires
|
|
// once at construction and never again — before Yarn has set
|
|
// $player_name_full. Refresh on show or the tab stays stale forever.
|
|
tabs[0].onShown.AddListener(charView.Refresh);
|
|
tabs[1] = new MenuTabBar.TabEntry
|
|
{
|
|
button = skillsBtn,
|
|
panel = skillsPanel.GetComponent<CanvasGroup>(),
|
|
onShown = new UnityEngine.Events.UnityEvent(),
|
|
};
|
|
tabs[1].onShown.AddListener(skillsView.Refresh);
|
|
tabs[2] = new MenuTabBar.TabEntry
|
|
{
|
|
button = journalBtn,
|
|
panel = journalPanel.GetComponent<CanvasGroup>(),
|
|
onShown = new UnityEngine.Events.UnityEvent(),
|
|
};
|
|
tabBar.SetTabs(tabs);
|
|
|
|
var player = GameObject.Find("PlayerArmature");
|
|
var menuController = canvasGo.AddComponent<PlayerMenuController>();
|
|
SetPrivate(menuController, "menuRoot", canvasGo);
|
|
SetPrivate(menuController, "menuInput", player != null ? player.GetComponent<PlayerMenuInput>() : null);
|
|
SetPrivate(menuController, "controlLock", player != null ? player.GetComponent<PlayerControlLock>() : null);
|
|
SetPrivate(menuController, "tabBar", tabBar);
|
|
|
|
// Portrait click
|
|
var portraitButton = FindFirstObjectByType<PortraitHudView>()?.GetComponentInChildren<Button>(true);
|
|
if (portraitButton != null)
|
|
{
|
|
portraitButton.onClick.AddListener(menuController.Open);
|
|
}
|
|
}
|
|
|
|
SkillRowView BuildSkillRowPrefab(Transform parent)
|
|
{
|
|
var root = new GameObject("SkillRow", typeof(RectTransform), typeof(SkillRowView), typeof(HorizontalLayoutGroup));
|
|
root.transform.SetParent(parent, false);
|
|
var hlg = root.GetComponent<HorizontalLayoutGroup>();
|
|
hlg.padding = new RectOffset(8, 8, 4, 4);
|
|
hlg.spacing = 12f;
|
|
hlg.childAlignment = TextAnchor.MiddleLeft;
|
|
hlg.childControlHeight = true;
|
|
hlg.childControlWidth = true;
|
|
hlg.childForceExpandHeight = true;
|
|
|
|
var stripe = new GameObject("Accent", typeof(RectTransform), typeof(Image), typeof(LayoutElement));
|
|
stripe.transform.SetParent(root.transform, false);
|
|
stripe.GetComponent<LayoutElement>().preferredWidth = 6f;
|
|
stripe.GetComponent<Image>().color = Color.white;
|
|
|
|
var name = CreateTmp("Name", root.transform, 24f, FontStyles.Bold);
|
|
name.GetComponent<LayoutElement>().flexibleWidth = 1f;
|
|
|
|
var bar = new GameObject("Bar", typeof(RectTransform), typeof(Image), typeof(LayoutElement));
|
|
bar.transform.SetParent(root.transform, false);
|
|
bar.GetComponent<Image>().color = new Color(0.2f, 0.2f, 0.22f);
|
|
bar.GetComponent<LayoutElement>().preferredWidth = 160f;
|
|
bar.GetComponent<LayoutElement>().preferredHeight = 16f;
|
|
|
|
var fill = new GameObject("Fill", typeof(RectTransform), typeof(Image));
|
|
fill.transform.SetParent(bar.transform, false);
|
|
Stretch(fill.GetComponent<RectTransform>());
|
|
var fillImage = fill.GetComponent<Image>();
|
|
fillImage.type = Image.Type.Filled;
|
|
fillImage.fillMethod = Image.FillMethod.Horizontal;
|
|
fillImage.color = Color.white;
|
|
|
|
var rank = CreateTmp("Rank", root.transform, 24f, FontStyles.Normal);
|
|
rank.alignment = TextAlignmentOptions.Right;
|
|
rank.GetComponent<LayoutElement>().preferredWidth = 40f;
|
|
|
|
var view = root.GetComponent<SkillRowView>();
|
|
SetPrivate(view, "nameLabel", name);
|
|
SetPrivate(view, "rankLabel", rank);
|
|
SetPrivate(view, "barFill", fillImage);
|
|
SetPrivate(view, "accentStripe", stripe.GetComponent<Image>());
|
|
return view;
|
|
}
|
|
|
|
void EnsureCameraFraming()
|
|
{
|
|
var vcam = GameObject.Find("CinemachineCamera");
|
|
if (vcam == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var framing = vcam.GetComponent<DialogueCameraFraming>();
|
|
if (framing == null)
|
|
{
|
|
framing = vcam.AddComponent<DialogueCameraFraming>();
|
|
}
|
|
|
|
SetPrivate(framing, "composer", vcam.GetComponent<CinemachinePositionComposer>());
|
|
SetPrivate(framing, "dialogueScreenX", -0.17f);
|
|
}
|
|
|
|
void WireDialogueRunner()
|
|
{
|
|
var runner = FindFirstObjectByType<DialogueRunner>();
|
|
var stream = FindFirstObjectByType<DialogueStreamPresenter>();
|
|
var options = FindFirstObjectByType<DialogueStreamOptionsPresenter>();
|
|
var advancer = FindFirstObjectByType<LineAdvancer>();
|
|
if (runner == null || stream == null || options == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var list = new System.Collections.Generic.List<DialoguePresenterBase>();
|
|
list.Add(stream);
|
|
list.Add(options);
|
|
if (advancer != null)
|
|
{
|
|
list.Add(advancer);
|
|
SetPrivate(advancer, "presenter", stream);
|
|
}
|
|
|
|
runner.DialoguePresenters = list;
|
|
}
|
|
|
|
void HidePackageDialogueCanvas()
|
|
{
|
|
var dialogueSystem = GameObject.Find("Dialogue System");
|
|
if (dialogueSystem == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var packageCanvas = dialogueSystem.transform.Find("Canvas");
|
|
if (packageCanvas == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var banner = packageCanvas.Find("Skill Check Banner");
|
|
var streamCanvas = GameObject.Find("DialogueStreamCanvas");
|
|
if (banner != null && streamCanvas != null)
|
|
{
|
|
banner.SetParent(streamCanvas.transform, false);
|
|
var bannerRt = banner.GetComponent<RectTransform>();
|
|
if (bannerRt != null)
|
|
{
|
|
bannerRt.anchorMin = new Vector2(0f, 0.85f);
|
|
bannerRt.anchorMax = new Vector2(0.66f, 1f);
|
|
bannerRt.offsetMin = new Vector2(16f, 0f);
|
|
bannerRt.offsetMax = new Vector2(-16f, -16f);
|
|
}
|
|
}
|
|
|
|
packageCanvas.gameObject.SetActive(false);
|
|
}
|
|
|
|
static GameObject CreateCanvas(string name, int sortingOrder)
|
|
{
|
|
var go = new GameObject(name, typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
|
|
var canvas = go.GetComponent<Canvas>();
|
|
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
canvas.sortingOrder = sortingOrder;
|
|
var scaler = go.GetComponent<CanvasScaler>();
|
|
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
|
scaler.referenceResolution = new Vector2(1920f, 1080f);
|
|
scaler.matchWidthOrHeight = 0.5f;
|
|
return go;
|
|
}
|
|
|
|
static GameObject CreateTabPanel(string name, Transform parent)
|
|
{
|
|
var go = new GameObject(name, typeof(RectTransform), typeof(CanvasGroup));
|
|
go.transform.SetParent(parent, false);
|
|
Stretch(go.GetComponent<RectTransform>());
|
|
var group = go.GetComponent<CanvasGroup>();
|
|
group.alpha = 0f;
|
|
group.interactable = false;
|
|
group.blocksRaycasts = false;
|
|
return go;
|
|
}
|
|
|
|
static Button CreateTabButton(string label, Transform parent)
|
|
{
|
|
var go = new GameObject(label, typeof(RectTransform), typeof(Image), typeof(Button), typeof(LayoutElement));
|
|
go.transform.SetParent(parent, false);
|
|
go.GetComponent<Image>().color = new Color(0.2f, 0.2f, 0.25f, 1f);
|
|
var text = CreateTmp("Label", go.transform, 22f, FontStyles.Bold);
|
|
text.text = label;
|
|
text.alignment = TextAlignmentOptions.Center;
|
|
Stretch(text.rectTransform);
|
|
return go.GetComponent<Button>();
|
|
}
|
|
|
|
static TextMeshProUGUI CreateTmp(string name, Transform parent, float size, FontStyles style)
|
|
{
|
|
var go = new GameObject(name, typeof(RectTransform), typeof(TextMeshProUGUI), typeof(LayoutElement));
|
|
go.transform.SetParent(parent, false);
|
|
var tmp = go.GetComponent<TextMeshProUGUI>();
|
|
tmp.fontSize = size;
|
|
tmp.fontStyle = style;
|
|
tmp.color = Color.white;
|
|
tmp.enableWordWrapping = true;
|
|
tmp.text = name;
|
|
// No LayoutElement.preferredHeight — it would pin wrapped text to a
|
|
// single line's height and make stream rows overlap. See the same
|
|
// note in UILayerSetup.CreateTmp.
|
|
return tmp;
|
|
}
|
|
|
|
static void Stretch(RectTransform rt, float pad = 0f)
|
|
{
|
|
rt.anchorMin = Vector2.zero;
|
|
rt.anchorMax = Vector2.one;
|
|
rt.offsetMin = new Vector2(pad, pad);
|
|
rt.offsetMax = new Vector2(-pad, -pad);
|
|
}
|
|
|
|
static void ConfigureVertical(GameObject go)
|
|
{
|
|
var rt = go.GetComponent<RectTransform>();
|
|
rt.anchorMin = new Vector2(0f, 0f);
|
|
rt.anchorMax = new Vector2(1f, 0f);
|
|
rt.pivot = new Vector2(0.5f, 0f);
|
|
// See UILayerSetup.ConfigureVerticalContent: a script-added
|
|
// RectTransform starts at sizeDelta (100,100), which overhangs 50px
|
|
// off each side under stretch anchors and gets sheared by the mask.
|
|
rt.sizeDelta = new Vector2(0f, rt.sizeDelta.y);
|
|
rt.anchoredPosition = Vector2.zero;
|
|
var vlg = go.GetComponent<VerticalLayoutGroup>();
|
|
vlg.childControlHeight = true;
|
|
vlg.childControlWidth = true;
|
|
vlg.childForceExpandHeight = false;
|
|
vlg.childForceExpandWidth = true;
|
|
vlg.spacing = 4f;
|
|
go.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
}
|
|
|
|
static void SetPrivate(object target, string fieldName, object value)
|
|
{
|
|
var type = target.GetType();
|
|
while (type != null)
|
|
{
|
|
var field = type.GetField(fieldName, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
|
|
if (field != null)
|
|
{
|
|
field.SetValue(target, value);
|
|
return;
|
|
}
|
|
|
|
type = type.BaseType;
|
|
}
|
|
|
|
Debug.LogWarning($"[UILayerBootstrap] Could not set field '{fieldName}' on {target.GetType().Name}");
|
|
}
|
|
}
|
|
}
|