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 { /// /// 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. /// [DefaultExecutionOrder(-100)] public sealed class UILayerBootstrap : MonoBehaviour { [SerializeField] bool buildIfMissing = true; void Awake() { EnsureFoundation(); if (!buildIfMissing) { return; } if (FindFirstObjectByType() == null) { BuildDialogueStream(); } if (FindFirstObjectByType() == null) { BuildHud(); } if (FindFirstObjectByType() == null) { BuildMenu(); } EnsureCameraFraming(); WireDialogueRunner(); HidePackageDialogueCanvas(); } void EnsureFoundation() { if (UIStateController.Instance == null && FindFirstObjectByType() == null) { var go = GameObject.Find("UI System") ?? new GameObject("UI System"); go.AddComponent(); } var player = GameObject.Find("PlayerArmature"); if (player == null) { return; } if (player.GetComponent() == null) { player.AddComponent(); } if (player.GetComponent() == null) { player.AddComponent(); } var inputs = player.GetComponent(); if (inputs != null) { inputs.cursorLocked = false; Cursor.lockState = CursorLockMode.None; Cursor.visible = true; } var transition = FindFirstObjectByType(); if (transition != null) { var soLock = player.GetComponent(); // 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.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(); panelRt.anchorMin = new Vector2(0.66f, 0f); panelRt.anchorMax = Vector2.one; panelRt.offsetMin = Vector2.zero; panelRt.offsetMax = Vector2.zero; panel.GetComponent().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(), 8f); scrollGo.GetComponent().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()); viewport.GetComponent().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(); contentRt.anchorMin = new Vector2(0f, 0f); contentRt.anchorMax = new Vector2(1f, 0f); contentRt.pivot = new Vector2(0.5f, 0f); var vlg = content.GetComponent(); vlg.childAlignment = TextAnchor.LowerCenter; vlg.childControlHeight = true; vlg.childControlWidth = true; vlg.childForceExpandHeight = false; vlg.childForceExpandWidth = true; vlg.spacing = 6f; content.GetComponent().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(); optionsGroup.alpha = 0f; optionsGroup.interactable = false; optionsGroup.blocksRaycasts = false; var scroll = scrollGo.GetComponent(); scroll.content = contentRt; scroll.viewport = viewport.GetComponent(); 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(); SetPrivate(stream, "messageRowPrefab", rowPrefab); SetPrivate(stream, "messageContainer", messages.GetComponent()); SetPrivate(stream, "scrollRect", scroll); OptionItem yarnOption = null; #if UNITY_EDITOR yarnOption = UnityEditor.AssetDatabase.LoadAssetAtPath( "Packages/dev.yarnspinner.unity/Prefabs/Option Item.prefab"); #endif if (yarnOption == null) { yarnOption = BuildFallbackOptionItem(templates.transform); } var optionsPresenter = canvasGo.AddComponent(); SetPrivate(optionsPresenter, "optionViewPrefab", yarnOption); SetPrivate(optionsPresenter, "optionsContainer", options.transform); SetPrivate(optionsPresenter, "canvasGroup", optionsGroup); var transition = FindFirstObjectByType(); if (transition != null) { SetPrivate(transition, "dialogueUI", canvasGo); } // Interactors used to be pushed this canvas here. DialogueInteractor resolves // it from the runner at the point of use instead, so there is nothing to push. } 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(); 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().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(); 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().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(), "text", text); return go.GetComponent(); } 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(); 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().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("portrait_placeholder"); #if UNITY_EDITOR if (sprite == null) { sprite = UnityEditor.AssetDatabase.LoadAssetAtPath( "Assets/Art/UI/Placeholder/Resources/portrait_placeholder.png"); } #endif if (sprite != null) { panel.GetComponent().sprite = sprite; panel.GetComponent().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(); SetPrivate(view, "panelRoot", panel); } void BuildMenu() { var canvasGo = CreateCanvas("PlayerMenuCanvas", 20); var rootGroup = canvasGo.AddComponent(); 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()); dimmer.GetComponent().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(); 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().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(); 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(); 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(); 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(); charVlg.spacing = 12f; charVlg.padding = new RectOffset(8, 8, 8, 8); charVlg.childControlWidth = true; charVlg.childControlHeight = true; charVlg.childForceExpandWidth = true; var charView = characterPanel.AddComponent(); 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()); ConfigureVertical(skillsContent); skillsContent.GetComponent().anchorMin = new Vector2(0f, 1f); skillsContent.GetComponent().anchorMax = new Vector2(1f, 1f); skillsContent.GetComponent().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(); SetPrivate(skillsView, "contentRoot", skillsContent.GetComponent()); SetPrivate(skillsView, "rowPrefab", rowPrefab); SetPrivate(skillsView, "axisHeaderPrefab", axisHeader); SetPrivate(skillsView, "displayMaxRank", 6); // Journal var journalView = journalPanel.AddComponent(); 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(); var tabs = new MenuTabBar.TabEntry[3]; tabs[0] = new MenuTabBar.TabEntry { button = charBtn, panel = characterPanel.GetComponent(), 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(), onShown = new UnityEngine.Events.UnityEvent(), }; tabs[1].onShown.AddListener(skillsView.Refresh); tabs[2] = new MenuTabBar.TabEntry { button = journalBtn, panel = journalPanel.GetComponent(), onShown = new UnityEngine.Events.UnityEvent(), }; tabBar.SetTabs(tabs); var player = GameObject.Find("PlayerArmature"); var menuController = canvasGo.AddComponent(); SetPrivate(menuController, "menuRoot", canvasGo); SetPrivate(menuController, "menuInput", player != null ? player.GetComponent() : null); SetPrivate(menuController, "controlLock", player != null ? player.GetComponent() : null); SetPrivate(menuController, "tabBar", tabBar); // Portrait click var portraitButton = FindFirstObjectByType()?.GetComponentInChildren