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