diff --git a/NightclubArcadia/Assets/Art.meta b/NightclubArcadia/Assets/Art.meta
new file mode 100644
index 0000000..e078b6a
--- /dev/null
+++ b/NightclubArcadia/Assets/Art.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 98c69c750f094ae79e2f9362c4422647
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/NightclubArcadia/Assets/Art/UI.meta b/NightclubArcadia/Assets/Art/UI.meta
new file mode 100644
index 0000000..a5c5890
--- /dev/null
+++ b/NightclubArcadia/Assets/Art/UI.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6270e22570864d7c862434241018328f
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/NightclubArcadia/Assets/Art/UI/Placeholder.meta b/NightclubArcadia/Assets/Art/UI/Placeholder.meta
new file mode 100644
index 0000000..fcd772b
--- /dev/null
+++ b/NightclubArcadia/Assets/Art/UI/Placeholder.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: bffd4617fb74418f9eb0d0d6378cf79a
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/NightclubArcadia/Assets/Art/UI/Placeholder/Resources.meta b/NightclubArcadia/Assets/Art/UI/Placeholder/Resources.meta
new file mode 100644
index 0000000..2f6bf6a
--- /dev/null
+++ b/NightclubArcadia/Assets/Art/UI/Placeholder/Resources.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ff84645ddce3641b0a7e9c68db56c8db
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/NightclubArcadia/Assets/Art/UI/Placeholder/Resources/portrait_placeholder.png b/NightclubArcadia/Assets/Art/UI/Placeholder/Resources/portrait_placeholder.png
new file mode 100644
index 0000000..70d3a57
Binary files /dev/null and b/NightclubArcadia/Assets/Art/UI/Placeholder/Resources/portrait_placeholder.png differ
diff --git a/NightclubArcadia/Assets/Art/UI/Placeholder/Resources/portrait_placeholder.png.meta b/NightclubArcadia/Assets/Art/UI/Placeholder/Resources/portrait_placeholder.png.meta
new file mode 100644
index 0000000..2f3bd4a
--- /dev/null
+++ b/NightclubArcadia/Assets/Art/UI/Placeholder/Resources/portrait_placeholder.png.meta
@@ -0,0 +1,84 @@
+fileFormatVersion: 2
+guid: a1b2c3d4e5f60718293a4b5c6d7e8f90
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 13
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 0
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ flipGreenChannel: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ vTOnly: 0
+ ignoreMipmapLimit: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 1
+ wrapV: 1
+ wrapW: 1
+ nPOTScale: 0
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 1
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 1
+ spriteTessellationDetail: -1
+ textureType: 8
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings: []
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ physicsShape: []
+ bones: []
+ spriteID: 5e97eb03825dee720800000000000000
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ spritePackingTag:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/NightclubArcadia/Assets/Editor/UILayerSetup.cs b/NightclubArcadia/Assets/Editor/UILayerSetup.cs
new file mode 100644
index 0000000..1c57e65
--- /dev/null
+++ b/NightclubArcadia/Assets/Editor/UILayerSetup.cs
@@ -0,0 +1,818 @@
+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