From cba87dcb3109aa85cb60385214d499aafd27f098 Mon Sep 17 00:00:00 2001 From: grabowskil Date: Tue, 25 Aug 2026 19:03:42 +0200 Subject: [PATCH] add UI layer: dialogue stream, HUD, player menu, and skills panel Runtime UI under Scripts/UI: a UIStateController owning menu/dialogue state, a PlayerControlLock so opening a panel suppresses world input, panel visibility helpers, and the menu input binding. Views: the dialogue stream presenter and its options presenter and message row, the portrait HUD, and the player menu with character, journal, and tab-bar views. Skills gains SkillsPanelView and SkillRowView, which is why the Skills asmdef now references UnityEngine.UI. Prefabs/UI holds the authored canvases (dialogue stream, HUD, player menu) and their rows. UILayerBootstrap remains a hand-added runtime fallback for scenes where those prefabs are not instantiated; it deliberately does not auto-spawn. Editor/UILayerSetup.cs is the one-shot menu that builds and wires those prefabs into a scene, with UILayerSetupAutoRun reminding after a script reload. Art/UI/Placeholder carries the placeholder portrait the HUD falls back to. Co-Authored-By: Claude Opus 5 --- NightclubArcadia/Assets/Art.meta | 8 + NightclubArcadia/Assets/Art/UI.meta | 8 + .../Assets/Art/UI/Placeholder.meta | 8 + .../Assets/Art/UI/Placeholder/Resources.meta | 8 + .../Resources/portrait_placeholder.png | Bin 0 -> 2734 bytes .../Resources/portrait_placeholder.png.meta | 84 + .../Assets/Editor/UILayerSetup.cs | 818 ++++++ .../Assets/Editor/UILayerSetup.cs.meta | 11 + .../Assets/Editor/UILayerSetupAutoRun.cs | 48 + .../Assets/Editor/UILayerSetupAutoRun.cs.meta | 11 + NightclubArcadia/Assets/Prefabs.meta | 8 + NightclubArcadia/Assets/Prefabs/UI.meta | 8 + .../Prefabs/UI/DialogueMessageRow.prefab | 413 +++ .../Prefabs/UI/DialogueMessageRow.prefab.meta | 7 + .../Prefabs/UI/DialogueStreamCanvas.prefab | 673 +++++ .../UI/DialogueStreamCanvas.prefab.meta | 7 + .../Assets/Prefabs/UI/HudCanvas.prefab | 390 +++ .../Assets/Prefabs/UI/HudCanvas.prefab.meta | 7 + .../Assets/Prefabs/UI/PlayerMenuCanvas.prefab | 2510 +++++++++++++++++ .../Prefabs/UI/PlayerMenuCanvas.prefab.meta | 7 + .../Assets/Prefabs/UI/SkillRow.prefab | 669 +++++ .../Assets/Prefabs/UI/SkillRow.prefab.meta | 7 + .../Skills/NightclubArcadia.Skills.asmdef | 3 +- .../Assets/Scripts/Skills/UI/SkillRowView.cs | 50 + .../Scripts/Skills/UI/SkillRowView.cs.meta | 11 + .../Scripts/Skills/UI/SkillsPanelView.cs | 88 + .../Scripts/Skills/UI/SkillsPanelView.cs.meta | 11 + NightclubArcadia/Assets/Scripts/UI.meta | 8 + .../Assets/Scripts/UI/Dialogue.meta | 8 + .../Scripts/UI/Dialogue/DialogueMessageRow.cs | 53 + .../UI/Dialogue/DialogueMessageRow.cs.meta | 11 + .../DialogueStreamOptionsPresenter.cs | 173 ++ .../DialogueStreamOptionsPresenter.cs.meta | 11 + .../UI/Dialogue/DialogueStreamPresenter.cs | 133 + .../Dialogue/DialogueStreamPresenter.cs.meta | 11 + NightclubArcadia/Assets/Scripts/UI/HUD.meta | 8 + .../Assets/Scripts/UI/HUD/PortraitHudView.cs | 65 + .../Scripts/UI/HUD/PortraitHudView.cs.meta | 11 + NightclubArcadia/Assets/Scripts/UI/Menu.meta | 8 + .../Scripts/UI/Menu/CharacterPanelView.cs | 82 + .../UI/Menu/CharacterPanelView.cs.meta | 11 + .../Scripts/UI/Menu/JournalPanelView.cs | 21 + .../Scripts/UI/Menu/JournalPanelView.cs.meta | 11 + .../Assets/Scripts/UI/Menu/MenuTabBar.cs | 93 + .../Assets/Scripts/UI/Menu/MenuTabBar.cs.meta | 11 + .../Scripts/UI/Menu/PlayerMenuController.cs | 139 + .../UI/Menu/PlayerMenuController.cs.meta | 11 + .../Assets/Scripts/UI/PanelVisibility.cs | 67 + .../Assets/Scripts/UI/PanelVisibility.cs.meta | 11 + .../Assets/Scripts/UI/PlayerControlLock.cs | 101 + .../Scripts/UI/PlayerControlLock.cs.meta | 11 + .../Assets/Scripts/UI/PlayerMenuInput.cs | 24 + .../Assets/Scripts/UI/PlayerMenuInput.cs.meta | 11 + .../Assets/Scripts/UI/UILayerBootstrap.cs | 646 +++++ .../Scripts/UI/UILayerBootstrap.cs.meta | 11 + .../Assets/Scripts/UI/UIStateController.cs | 63 + .../Scripts/UI/UIStateController.cs.meta | 11 + 57 files changed, 7707 insertions(+), 1 deletion(-) create mode 100644 NightclubArcadia/Assets/Art.meta create mode 100644 NightclubArcadia/Assets/Art/UI.meta create mode 100644 NightclubArcadia/Assets/Art/UI/Placeholder.meta create mode 100644 NightclubArcadia/Assets/Art/UI/Placeholder/Resources.meta create mode 100644 NightclubArcadia/Assets/Art/UI/Placeholder/Resources/portrait_placeholder.png create mode 100644 NightclubArcadia/Assets/Art/UI/Placeholder/Resources/portrait_placeholder.png.meta create mode 100644 NightclubArcadia/Assets/Editor/UILayerSetup.cs create mode 100644 NightclubArcadia/Assets/Editor/UILayerSetup.cs.meta create mode 100644 NightclubArcadia/Assets/Editor/UILayerSetupAutoRun.cs create mode 100644 NightclubArcadia/Assets/Editor/UILayerSetupAutoRun.cs.meta create mode 100644 NightclubArcadia/Assets/Prefabs.meta create mode 100644 NightclubArcadia/Assets/Prefabs/UI.meta create mode 100644 NightclubArcadia/Assets/Prefabs/UI/DialogueMessageRow.prefab create mode 100644 NightclubArcadia/Assets/Prefabs/UI/DialogueMessageRow.prefab.meta create mode 100644 NightclubArcadia/Assets/Prefabs/UI/DialogueStreamCanvas.prefab create mode 100644 NightclubArcadia/Assets/Prefabs/UI/DialogueStreamCanvas.prefab.meta create mode 100644 NightclubArcadia/Assets/Prefabs/UI/HudCanvas.prefab create mode 100644 NightclubArcadia/Assets/Prefabs/UI/HudCanvas.prefab.meta create mode 100644 NightclubArcadia/Assets/Prefabs/UI/PlayerMenuCanvas.prefab create mode 100644 NightclubArcadia/Assets/Prefabs/UI/PlayerMenuCanvas.prefab.meta create mode 100644 NightclubArcadia/Assets/Prefabs/UI/SkillRow.prefab create mode 100644 NightclubArcadia/Assets/Prefabs/UI/SkillRow.prefab.meta create mode 100644 NightclubArcadia/Assets/Scripts/Skills/UI/SkillRowView.cs create mode 100644 NightclubArcadia/Assets/Scripts/Skills/UI/SkillRowView.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/Skills/UI/SkillsPanelView.cs create mode 100644 NightclubArcadia/Assets/Scripts/Skills/UI/SkillsPanelView.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/Dialogue.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/Dialogue/DialogueMessageRow.cs create mode 100644 NightclubArcadia/Assets/Scripts/UI/Dialogue/DialogueMessageRow.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/Dialogue/DialogueStreamOptionsPresenter.cs create mode 100644 NightclubArcadia/Assets/Scripts/UI/Dialogue/DialogueStreamOptionsPresenter.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/Dialogue/DialogueStreamPresenter.cs create mode 100644 NightclubArcadia/Assets/Scripts/UI/Dialogue/DialogueStreamPresenter.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/HUD.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/HUD/PortraitHudView.cs create mode 100644 NightclubArcadia/Assets/Scripts/UI/HUD/PortraitHudView.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/Menu.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/Menu/CharacterPanelView.cs create mode 100644 NightclubArcadia/Assets/Scripts/UI/Menu/CharacterPanelView.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/Menu/JournalPanelView.cs create mode 100644 NightclubArcadia/Assets/Scripts/UI/Menu/JournalPanelView.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/Menu/MenuTabBar.cs create mode 100644 NightclubArcadia/Assets/Scripts/UI/Menu/MenuTabBar.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/Menu/PlayerMenuController.cs create mode 100644 NightclubArcadia/Assets/Scripts/UI/Menu/PlayerMenuController.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/PanelVisibility.cs create mode 100644 NightclubArcadia/Assets/Scripts/UI/PanelVisibility.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/PlayerControlLock.cs create mode 100644 NightclubArcadia/Assets/Scripts/UI/PlayerControlLock.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/PlayerMenuInput.cs create mode 100644 NightclubArcadia/Assets/Scripts/UI/PlayerMenuInput.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/UILayerBootstrap.cs create mode 100644 NightclubArcadia/Assets/Scripts/UI/UILayerBootstrap.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/UI/UIStateController.cs create mode 100644 NightclubArcadia/Assets/Scripts/UI/UIStateController.cs.meta 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 0000000000000000000000000000000000000000..70d3a57dd398628cfb317354975021b387ba9767 GIT binary patch literal 2734 zcmcgu&1(}u6n`^a(GRFQ}kKmH0& z;$065l^z8_M7(IJ2SG%OB8!CT@6GHqNwaNJ8$ArWvor7gKHi&Q?@Uik^yd%fiKu__ zY;=K0;#HEL!{f)?>t96qtCLY>X71gWHd*YDz^q#KNuDz8@FBcxp=O{Th ziXfu0iKc)^$O)Q=%EY6PgOhVT7=Vlvl02ur$NM>-ZD`dbl#1fyE~Bjv0x3?8OoZ6CbZQuK3JVdK zls0N$Qmuz(eT12;Pk7q(|4K1Cz)Fb|4ye}e4RS*E$Vc*;NgfC%xmvGy3M6=>$=B}l zy=AW=%f?Q7Hu5RFvZYpNAux$illv78)(%Qp~b9|a9qG~XNrd|;DY`>C}`M(MYK|6j5Zt( z%|p5gCnF%z$CtsQY|@TO%RD2aEN#Ht=8Pdas+B^`_!L;FUa8&`@azD##z~K)TY9zE zqncqJ$lGA9B->l|3pa480}i8*nbTPO4Gs+6yE(~XuR2_tZzGPZ!Bq=^>B1LLurJO zB#u%busx|AmiEEdN!}rQ=F(sB +/// 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