using System.IO; using NightclubArcadia.Cinematics; using NightclubArcadia.Dialogue; using NightclubArcadia.Skills; using NightclubArcadia.UI; using NightclubArcadia.UI.Dialogue; using NightclubArcadia.UI.HUD; using NightclubArcadia.UI.Menu; using StarterAssets; using TMPro; using Unity.Cinemachine; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.UI; using Yarn.Unity; /// /// Builds UI prefabs and wires them into DialogueTest.unity. /// Run via Tools → Nightclub Arcadia → Set Up UI Layer. /// public static class UILayerSetup { const string ScenePath = "Assets/Scenes/DialogueTest.unity"; const string PrefabFolder = "Assets/Prefabs/UI"; const string ArtFolder = "Assets/Art/UI/Placeholder"; // Nested Resources folder: UILayerBootstrap builds the HUD at runtime and // can only reach the sprite through Resources.Load. Keeping it under // Art/UI/Placeholder preserves the organisation while making it // build-safe — an AssetDatabase-only path is null outside the editor. const string PortraitFolder = ArtFolder + "/Resources"; const string PortraitPath = PortraitFolder + "/portrait_placeholder.png"; const string MessageRowPrefabPath = PrefabFolder + "/DialogueMessageRow.prefab"; const string StreamCanvasPrefabPath = PrefabFolder + "/DialogueStreamCanvas.prefab"; const string HudCanvasPrefabPath = PrefabFolder + "/HudCanvas.prefab"; const string SkillRowPrefabPath = PrefabFolder + "/SkillRow.prefab"; const string MenuCanvasPrefabPath = PrefabFolder + "/PlayerMenuCanvas.prefab"; [MenuItem("Tools/Nightclub Arcadia/Set Up UI Layer")] public static void SetUp() { EnsureFolders(); CreatePortraitPlaceholder(); var messageRow = CreateMessageRowPrefab(); var skillRow = CreateSkillRowPrefab(); var streamCanvas = CreateDialogueStreamCanvasPrefab(messageRow); var hudCanvas = CreateHudCanvasPrefab(); var menuCanvas = CreatePlayerMenuCanvasPrefab(skillRow); WireScene(streamCanvas, hudCanvas, menuCanvas); AssetDatabase.SaveAssets(); Debug.Log("[UILayerSetup] UI layer setup complete."); } static void EnsureFolders() { EnsureFolder("Assets/Prefabs"); EnsureFolder(PrefabFolder); EnsureFolder("Assets/Art"); EnsureFolder("Assets/Art/UI"); EnsureFolder(ArtFolder); EnsureFolder(PortraitFolder); } static void EnsureFolder(string path) { if (AssetDatabase.IsValidFolder(path)) { return; } var parent = Path.GetDirectoryName(path)?.Replace('\\', '/'); var name = Path.GetFileName(path); if (!string.IsNullOrEmpty(parent) && !string.IsNullOrEmpty(name)) { AssetDatabase.CreateFolder(parent, name); } } static void CreatePortraitPlaceholder() { if (File.Exists(PortraitPath)) { return; } var tex = new Texture2D(512, 512, TextureFormat.RGBA32, false); var dark = new Color(0.12f, 0.12f, 0.14f, 1f); var mid = new Color(0.35f, 0.35f, 0.4f, 1f); for (var y = 0; y < 512; y++) { for (var x = 0; x < 512; x++) { var dx = (x - 256) / 256f; var dy = (y - 280) / 256f; var inHead = dx * dx + dy * dy < 0.12f; var inBody = Mathf.Abs(dx) < 0.22f && y < 220 && y > 40; tex.SetPixel(x, y, inHead || inBody ? mid : dark); } } tex.Apply(); File.WriteAllBytes(PortraitPath, tex.EncodeToPNG()); Object.DestroyImmediate(tex); AssetDatabase.Refresh(); var importer = AssetImporter.GetAtPath(PortraitPath) as TextureImporter; if (importer != null) { importer.textureType = TextureImporterType.Sprite; importer.spritePixelsPerUnit = 100; importer.SaveAndReimport(); } } static GameObject CreateMessageRowPrefab() { var root = new GameObject("DialogueMessageRow", typeof(RectTransform), typeof(DialogueMessageRow)); var rootRt = root.GetComponent(); rootRt.sizeDelta = new Vector2(0f, 80f); var layout = root.AddComponent(); layout.padding = new RectOffset(12, 12, 8, 8); layout.spacing = 4f; layout.childControlHeight = true; layout.childControlWidth = true; layout.childForceExpandHeight = false; layout.childForceExpandWidth = true; var fitter = root.AddComponent(); fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained; var speakerGo = CreateTmp("Speaker", root.transform, 22f, FontStyles.Bold, TextAlignmentOptions.Left); var bodyGo = CreateTmp("Body", root.transform, 28f, FontStyles.Normal, TextAlignmentOptions.TopLeft); var row = root.GetComponent(); var so = new SerializedObject(row); so.FindProperty("speakerText").objectReferenceValue = speakerGo.GetComponent(); so.FindProperty("bodyText").objectReferenceValue = bodyGo.GetComponent(); so.FindProperty("speakerLabel").objectReferenceValue = speakerGo; so.ApplyModifiedPropertiesWithoutUndo(); var prefab = PrefabUtility.SaveAsPrefabAsset(root, MessageRowPrefabPath); Object.DestroyImmediate(root); return prefab; } static GameObject CreateSkillRowPrefab() { var root = new GameObject("SkillRow", typeof(RectTransform), typeof(SkillRowView)); var rootRt = root.GetComponent(); rootRt.sizeDelta = new Vector2(0f, 48f); var hlg = root.AddComponent(); hlg.padding = new RectOffset(8, 8, 4, 4); hlg.spacing = 12f; hlg.childAlignment = TextAnchor.MiddleLeft; hlg.childControlHeight = true; hlg.childControlWidth = true; hlg.childForceExpandHeight = true; hlg.childForceExpandWidth = false; var stripe = CreateUiImage("Accent", root.transform, new Color(1f, 1f, 1f, 1f)); stripe.GetComponent().preferredWidth = 6f; stripe.GetComponent().flexibleWidth = 0f; var nameGo = CreateTmp("Name", root.transform, 24f, FontStyles.Bold, TextAlignmentOptions.Left); nameGo.GetComponent().flexibleWidth = 1f; var barGo = new GameObject("Bar", typeof(RectTransform), typeof(Image), typeof(LayoutElement)); barGo.transform.SetParent(root.transform, false); barGo.GetComponent().color = new Color(0.2f, 0.2f, 0.22f, 1f); barGo.GetComponent().preferredWidth = 160f; barGo.GetComponent().flexibleWidth = 0f; barGo.GetComponent().preferredHeight = 16f; var fillGo = CreateUiImage("Fill", barGo.transform, Color.white); var fillRt = fillGo.GetComponent(); fillRt.anchorMin = Vector2.zero; fillRt.anchorMax = Vector2.one; fillRt.offsetMin = Vector2.zero; fillRt.offsetMax = Vector2.zero; var fillImage = fillGo.GetComponent(); fillImage.type = Image.Type.Filled; fillImage.fillMethod = Image.FillMethod.Horizontal; fillImage.fillAmount = 0.2f; Object.DestroyImmediate(fillGo.GetComponent()); var rankGo = CreateTmp("Rank", root.transform, 24f, FontStyles.Normal, TextAlignmentOptions.Right); rankGo.GetComponent().preferredWidth = 40f; rankGo.GetComponent().flexibleWidth = 0f; var view = root.GetComponent(); var so = new SerializedObject(view); so.FindProperty("nameLabel").objectReferenceValue = nameGo.GetComponent(); so.FindProperty("rankLabel").objectReferenceValue = rankGo.GetComponent(); so.FindProperty("barFill").objectReferenceValue = fillImage; so.FindProperty("accentStripe").objectReferenceValue = stripe.GetComponent(); so.ApplyModifiedPropertiesWithoutUndo(); var prefab = PrefabUtility.SaveAsPrefabAsset(root, SkillRowPrefabPath); Object.DestroyImmediate(root); return prefab; } static GameObject CreateDialogueStreamCanvasPrefab(GameObject messageRowPrefab) { var canvasGo = CreateCanvas("DialogueStreamCanvas", 5); var canvasGroup = canvasGo.AddComponent(); 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 = new Vector2(1f, 1f); 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); contentRt.sizeDelta = new Vector2(0f, 0f); var vlg = content.GetComponent(); vlg.childAlignment = TextAnchor.LowerCenter; vlg.childControlHeight = true; vlg.childControlWidth = true; vlg.childForceExpandHeight = false; vlg.childForceExpandWidth = true; vlg.spacing = 6f; vlg.padding = new RectOffset(4, 4, 8, 8); var csf = content.GetComponent(); csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize; var messages = new GameObject("Messages", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter)); messages.transform.SetParent(content.transform, false); ConfigureVerticalContent(messages); var options = new GameObject("Options", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter), typeof(CanvasGroup)); options.transform.SetParent(content.transform, false); ConfigureVerticalContent(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; scroll.movementType = ScrollRect.MovementType.Clamped; var streamPresenter = canvasGo.AddComponent(); var streamSo = new SerializedObject(streamPresenter); streamSo.FindProperty("messageRowPrefab").objectReferenceValue = messageRowPrefab.GetComponent(); streamSo.FindProperty("messageContainer").objectReferenceValue = messages.GetComponent(); streamSo.FindProperty("scrollRect").objectReferenceValue = scroll; streamSo.ApplyModifiedPropertiesWithoutUndo(); var optionPrefab = AssetDatabase.LoadAssetAtPath( "Packages/dev.yarnspinner.unity/Prefabs/Option Item.prefab"); var optionsPresenter = canvasGo.AddComponent(); var optSo = new SerializedObject(optionsPresenter); if (optionPrefab != null) { optSo.FindProperty("optionViewPrefab").objectReferenceValue = optionPrefab.GetComponent(); } optSo.FindProperty("optionsContainer").objectReferenceValue = options.transform; optSo.FindProperty("canvasGroup").objectReferenceValue = optionsGroup; optSo.ApplyModifiedPropertiesWithoutUndo(); // Start hidden — DialogueTransitionController shows it. canvasGroup.alpha = 0f; canvasGroup.interactable = false; canvasGroup.blocksRaycasts = false; var prefab = PrefabUtility.SaveAsPrefabAsset(canvasGo, StreamCanvasPrefabPath); Object.DestroyImmediate(canvasGo); return prefab; } static GameObject CreateHudCanvasPrefab() { var canvasGo = CreateCanvas("HudCanvas", 10); var portraitSprite = AssetDatabase.LoadAssetAtPath(PortraitPath); var panel = new GameObject("PortraitPanel", typeof(RectTransform), typeof(CanvasGroup), typeof(Image), typeof(Button)); panel.transform.SetParent(canvasGo.transform, false); var panelRt = panel.GetComponent(); panelRt.anchorMin = new Vector2(1f, 0f); panelRt.anchorMax = new Vector2(1f, 0f); panelRt.pivot = new Vector2(1f, 0f); panelRt.sizeDelta = new Vector2(220f, 280f); panelRt.anchoredPosition = new Vector2(-24f, 24f); panel.GetComponent().color = new Color(0.15f, 0.15f, 0.18f, 0.95f); if (portraitSprite != null) { panel.GetComponent().sprite = portraitSprite; panel.GetComponent().preserveAspect = true; } var hint = CreateTmp("TabHint", panel.transform, 18f, FontStyles.Normal, TextAlignmentOptions.Center); var hintRt = hint.GetComponent(); 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); hint.GetComponent().text = "Tab"; Object.DestroyImmediate(hint.GetComponent()); var view = canvasGo.AddComponent(); var so = new SerializedObject(view); so.FindProperty("panelRoot").objectReferenceValue = panel; so.ApplyModifiedPropertiesWithoutUndo(); var prefab = PrefabUtility.SaveAsPrefabAsset(canvasGo, HudCanvasPrefabPath); Object.DestroyImmediate(canvasGo); return prefab; } static GameObject CreatePlayerMenuCanvasPrefab(GameObject skillRowPrefab) { var canvasGo = CreateCanvas("PlayerMenuCanvas", 20); var rootGroup = canvasGo.AddComponent(); var dimmer = CreateUiImage("Dimmer", canvasGo.transform, new Color(0f, 0f, 0f, 0.6f)); Stretch(dimmer.GetComponent()); Object.DestroyImmediate(dimmer.GetComponent()); 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); tabBarRt.anchoredPosition = Vector2.zero; 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. Left // false, each tab button keeps its own 100px sizeDelta ("Character" // clips to "Charact") and childForceExpandWidth dumps all the leftover // room between them as a gap instead of widening them. 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 = new Vector2(0f, 0f); contentRt.anchorMax = new Vector2(1f, 1f); 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 content var charView = characterPanel.AddComponent(); var nameLabel = CreateTmp("Name", characterPanel.transform, 36f, FontStyles.Bold, TextAlignmentOptions.Left); var aliasLabel = CreateTmp("Alias", characterPanel.transform, 28f, FontStyles.Italic, TextAlignmentOptions.Left); var invLabel = CreateTmp("Inventory", characterPanel.transform, 24f, FontStyles.Normal, TextAlignmentOptions.Left); var charVlg = characterPanel.AddComponent(); charVlg.spacing = 12f; charVlg.padding = new RectOffset(8, 8, 8, 8); charVlg.childControlWidth = true; charVlg.childControlHeight = true; charVlg.childForceExpandWidth = true; charVlg.childForceExpandHeight = false; var charSo = new SerializedObject(charView); charSo.FindProperty("nameLabel").objectReferenceValue = nameLabel.GetComponent(); charSo.FindProperty("aliasLabel").objectReferenceValue = aliasLabel.GetComponent(); charSo.FindProperty("inventoryPlaceholder").objectReferenceValue = invLabel.GetComponent(); charSo.ApplyModifiedPropertiesWithoutUndo(); // Skills content var skillsScroll = new GameObject("SkillsScroll", typeof(RectTransform), typeof(ScrollRect)); skillsScroll.transform.SetParent(skillsPanel.transform, false); Stretch(skillsScroll.GetComponent()); var skillsViewport = new GameObject("Viewport", typeof(RectTransform), typeof(RectMask2D), typeof(Image)); skillsViewport.transform.SetParent(skillsScroll.transform, false); Stretch(skillsViewport.GetComponent()); skillsViewport.GetComponent().color = new Color(1f, 1f, 1f, 0.01f); var skillsContent = new GameObject("Content", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter)); skillsContent.transform.SetParent(skillsViewport.transform, false); ConfigureVerticalContent(skillsContent); var skillsContentRt = skillsContent.GetComponent(); skillsContentRt.anchorMin = new Vector2(0f, 1f); skillsContentRt.anchorMax = new Vector2(1f, 1f); skillsContentRt.pivot = new Vector2(0.5f, 1f); var skillsScrollRect = skillsScroll.GetComponent(); skillsScrollRect.content = skillsContentRt; skillsScrollRect.viewport = skillsViewport.GetComponent(); skillsScrollRect.horizontal = false; var axisHeaderTemplate = CreateTmp("AxisHeaderTemplate", skillsContent.transform, 20f, FontStyles.Bold, TextAlignmentOptions.Left); axisHeaderTemplate.SetActive(false); var skillsView = skillsPanel.AddComponent(); var skillsSo = new SerializedObject(skillsView); skillsSo.FindProperty("contentRoot").objectReferenceValue = skillsContentRt; skillsSo.FindProperty("rowPrefab").objectReferenceValue = skillRowPrefab.GetComponent(); skillsSo.FindProperty("axisHeaderPrefab").objectReferenceValue = axisHeaderTemplate.GetComponent(); skillsSo.FindProperty("displayMaxRank").intValue = 6; skillsSo.ApplyModifiedPropertiesWithoutUndo(); // Journal content var journalView = journalPanel.AddComponent(); var journalLabel = CreateTmp("Placeholder", journalPanel.transform, 28f, FontStyles.Italic, TextAlignmentOptions.Center); Stretch(journalLabel.GetComponent()); Object.DestroyImmediate(journalLabel.GetComponent()); var journalSo = new SerializedObject(journalView); journalSo.FindProperty("placeholderLabel").objectReferenceValue = journalLabel.GetComponent(); journalSo.ApplyModifiedPropertiesWithoutUndo(); var charBtn = CreateTabButton("Character", tabBarGo.transform); var skillsBtn = CreateTabButton("Skills", tabBarGo.transform); var journalBtn = CreateTabButton("Journal", tabBarGo.transform); var tabBar = tabBarGo.GetComponent(); var tabSo = new SerializedObject(tabBar); var tabsProp = tabSo.FindProperty("tabs"); tabsProp.arraySize = 3; // Character tab must refresh on show for the same reason as Skills: // panels toggle via CanvasGroup, so OnEnable never fires again. SetTabEntry(tabsProp.GetArrayElementAtIndex(0), charBtn.GetComponent