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