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 <[email protected]>
819 lines
37 KiB
C#
819 lines
37 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Builds UI prefabs and wires them into DialogueTest.unity.
|
|
/// Run via Tools → Nightclub Arcadia → Set Up UI Layer.
|
|
/// </summary>
|
|
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<RectTransform>();
|
|
rootRt.sizeDelta = new Vector2(0f, 80f);
|
|
|
|
var layout = root.AddComponent<VerticalLayoutGroup>();
|
|
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<ContentSizeFitter>();
|
|
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<DialogueMessageRow>();
|
|
var so = new SerializedObject(row);
|
|
so.FindProperty("speakerText").objectReferenceValue = speakerGo.GetComponent<TMP_Text>();
|
|
so.FindProperty("bodyText").objectReferenceValue = bodyGo.GetComponent<TMP_Text>();
|
|
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<RectTransform>();
|
|
rootRt.sizeDelta = new Vector2(0f, 48f);
|
|
|
|
var hlg = root.AddComponent<HorizontalLayoutGroup>();
|
|
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<LayoutElement>().preferredWidth = 6f;
|
|
stripe.GetComponent<LayoutElement>().flexibleWidth = 0f;
|
|
|
|
var nameGo = CreateTmp("Name", root.transform, 24f, FontStyles.Bold, TextAlignmentOptions.Left);
|
|
nameGo.GetComponent<LayoutElement>().flexibleWidth = 1f;
|
|
|
|
var barGo = new GameObject("Bar", typeof(RectTransform), typeof(Image), typeof(LayoutElement));
|
|
barGo.transform.SetParent(root.transform, false);
|
|
barGo.GetComponent<Image>().color = new Color(0.2f, 0.2f, 0.22f, 1f);
|
|
barGo.GetComponent<LayoutElement>().preferredWidth = 160f;
|
|
barGo.GetComponent<LayoutElement>().flexibleWidth = 0f;
|
|
barGo.GetComponent<LayoutElement>().preferredHeight = 16f;
|
|
|
|
var fillGo = CreateUiImage("Fill", barGo.transform, Color.white);
|
|
var fillRt = fillGo.GetComponent<RectTransform>();
|
|
fillRt.anchorMin = Vector2.zero;
|
|
fillRt.anchorMax = Vector2.one;
|
|
fillRt.offsetMin = Vector2.zero;
|
|
fillRt.offsetMax = Vector2.zero;
|
|
var fillImage = fillGo.GetComponent<Image>();
|
|
fillImage.type = Image.Type.Filled;
|
|
fillImage.fillMethod = Image.FillMethod.Horizontal;
|
|
fillImage.fillAmount = 0.2f;
|
|
Object.DestroyImmediate(fillGo.GetComponent<LayoutElement>());
|
|
|
|
var rankGo = CreateTmp("Rank", root.transform, 24f, FontStyles.Normal, TextAlignmentOptions.Right);
|
|
rankGo.GetComponent<LayoutElement>().preferredWidth = 40f;
|
|
rankGo.GetComponent<LayoutElement>().flexibleWidth = 0f;
|
|
|
|
var view = root.GetComponent<SkillRowView>();
|
|
var so = new SerializedObject(view);
|
|
so.FindProperty("nameLabel").objectReferenceValue = nameGo.GetComponent<TMP_Text>();
|
|
so.FindProperty("rankLabel").objectReferenceValue = rankGo.GetComponent<TMP_Text>();
|
|
so.FindProperty("barFill").objectReferenceValue = fillImage;
|
|
so.FindProperty("accentStripe").objectReferenceValue = stripe.GetComponent<Image>();
|
|
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<CanvasGroup>();
|
|
|
|
var panel = new GameObject("Panel", typeof(RectTransform), typeof(Image));
|
|
panel.transform.SetParent(canvasGo.transform, false);
|
|
var panelRt = panel.GetComponent<RectTransform>();
|
|
panelRt.anchorMin = new Vector2(0.66f, 0f);
|
|
panelRt.anchorMax = new Vector2(1f, 1f);
|
|
panelRt.offsetMin = Vector2.zero;
|
|
panelRt.offsetMax = Vector2.zero;
|
|
panel.GetComponent<Image>().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<RectTransform>(), 8f);
|
|
scrollGo.GetComponent<Image>().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<RectTransform>());
|
|
viewport.GetComponent<Image>().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<RectTransform>();
|
|
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<VerticalLayoutGroup>();
|
|
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<ContentSizeFitter>();
|
|
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<CanvasGroup>();
|
|
optionsGroup.alpha = 0f;
|
|
optionsGroup.interactable = false;
|
|
optionsGroup.blocksRaycasts = false;
|
|
|
|
var scroll = scrollGo.GetComponent<ScrollRect>();
|
|
scroll.content = contentRt;
|
|
scroll.viewport = viewport.GetComponent<RectTransform>();
|
|
scroll.horizontal = false;
|
|
scroll.vertical = true;
|
|
scroll.movementType = ScrollRect.MovementType.Clamped;
|
|
|
|
var streamPresenter = canvasGo.AddComponent<DialogueStreamPresenter>();
|
|
var streamSo = new SerializedObject(streamPresenter);
|
|
streamSo.FindProperty("messageRowPrefab").objectReferenceValue = messageRowPrefab.GetComponent<DialogueMessageRow>();
|
|
streamSo.FindProperty("messageContainer").objectReferenceValue = messages.GetComponent<RectTransform>();
|
|
streamSo.FindProperty("scrollRect").objectReferenceValue = scroll;
|
|
streamSo.ApplyModifiedPropertiesWithoutUndo();
|
|
|
|
var optionPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(
|
|
"Packages/dev.yarnspinner.unity/Prefabs/Option Item.prefab");
|
|
var optionsPresenter = canvasGo.AddComponent<DialogueStreamOptionsPresenter>();
|
|
var optSo = new SerializedObject(optionsPresenter);
|
|
if (optionPrefab != null)
|
|
{
|
|
optSo.FindProperty("optionViewPrefab").objectReferenceValue = optionPrefab.GetComponent<OptionItem>();
|
|
}
|
|
|
|
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<Sprite>(PortraitPath);
|
|
|
|
var panel = new GameObject("PortraitPanel", typeof(RectTransform), typeof(CanvasGroup), typeof(Image), typeof(Button));
|
|
panel.transform.SetParent(canvasGo.transform, false);
|
|
var panelRt = panel.GetComponent<RectTransform>();
|
|
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<Image>().color = new Color(0.15f, 0.15f, 0.18f, 0.95f);
|
|
if (portraitSprite != null)
|
|
{
|
|
panel.GetComponent<Image>().sprite = portraitSprite;
|
|
panel.GetComponent<Image>().preserveAspect = true;
|
|
}
|
|
|
|
var hint = CreateTmp("TabHint", panel.transform, 18f, FontStyles.Normal, TextAlignmentOptions.Center);
|
|
var hintRt = hint.GetComponent<RectTransform>();
|
|
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<TMP_Text>().text = "Tab";
|
|
Object.DestroyImmediate(hint.GetComponent<LayoutElement>());
|
|
|
|
var view = canvasGo.AddComponent<PortraitHudView>();
|
|
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<CanvasGroup>();
|
|
|
|
var dimmer = CreateUiImage("Dimmer", canvasGo.transform, new Color(0f, 0f, 0f, 0.6f));
|
|
Stretch(dimmer.GetComponent<RectTransform>());
|
|
Object.DestroyImmediate(dimmer.GetComponent<LayoutElement>());
|
|
|
|
var body = new GameObject("MenuBody", typeof(RectTransform), typeof(Image));
|
|
body.transform.SetParent(canvasGo.transform, false);
|
|
var bodyRt = body.GetComponent<RectTransform>();
|
|
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<Image>().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<RectTransform>();
|
|
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<HorizontalLayoutGroup>();
|
|
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<RectTransform>();
|
|
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<CharacterPanelView>();
|
|
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<VerticalLayoutGroup>();
|
|
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<TMP_Text>();
|
|
charSo.FindProperty("aliasLabel").objectReferenceValue = aliasLabel.GetComponent<TMP_Text>();
|
|
charSo.FindProperty("inventoryPlaceholder").objectReferenceValue = invLabel.GetComponent<TMP_Text>();
|
|
charSo.ApplyModifiedPropertiesWithoutUndo();
|
|
|
|
// Skills content
|
|
var skillsScroll = new GameObject("SkillsScroll", typeof(RectTransform), typeof(ScrollRect));
|
|
skillsScroll.transform.SetParent(skillsPanel.transform, false);
|
|
Stretch(skillsScroll.GetComponent<RectTransform>());
|
|
var skillsViewport = new GameObject("Viewport", typeof(RectTransform), typeof(RectMask2D), typeof(Image));
|
|
skillsViewport.transform.SetParent(skillsScroll.transform, false);
|
|
Stretch(skillsViewport.GetComponent<RectTransform>());
|
|
skillsViewport.GetComponent<Image>().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<RectTransform>();
|
|
skillsContentRt.anchorMin = new Vector2(0f, 1f);
|
|
skillsContentRt.anchorMax = new Vector2(1f, 1f);
|
|
skillsContentRt.pivot = new Vector2(0.5f, 1f);
|
|
var skillsScrollRect = skillsScroll.GetComponent<ScrollRect>();
|
|
skillsScrollRect.content = skillsContentRt;
|
|
skillsScrollRect.viewport = skillsViewport.GetComponent<RectTransform>();
|
|
skillsScrollRect.horizontal = false;
|
|
|
|
var axisHeaderTemplate = CreateTmp("AxisHeaderTemplate", skillsContent.transform, 20f, FontStyles.Bold, TextAlignmentOptions.Left);
|
|
axisHeaderTemplate.SetActive(false);
|
|
|
|
var skillsView = skillsPanel.AddComponent<SkillsPanelView>();
|
|
var skillsSo = new SerializedObject(skillsView);
|
|
skillsSo.FindProperty("contentRoot").objectReferenceValue = skillsContentRt;
|
|
skillsSo.FindProperty("rowPrefab").objectReferenceValue = skillRowPrefab.GetComponent<SkillRowView>();
|
|
skillsSo.FindProperty("axisHeaderPrefab").objectReferenceValue = axisHeaderTemplate.GetComponent<TMP_Text>();
|
|
skillsSo.FindProperty("displayMaxRank").intValue = 6;
|
|
skillsSo.ApplyModifiedPropertiesWithoutUndo();
|
|
|
|
// Journal content
|
|
var journalView = journalPanel.AddComponent<JournalPanelView>();
|
|
var journalLabel = CreateTmp("Placeholder", journalPanel.transform, 28f, FontStyles.Italic, TextAlignmentOptions.Center);
|
|
Stretch(journalLabel.GetComponent<RectTransform>());
|
|
Object.DestroyImmediate(journalLabel.GetComponent<LayoutElement>());
|
|
var journalSo = new SerializedObject(journalView);
|
|
journalSo.FindProperty("placeholderLabel").objectReferenceValue = journalLabel.GetComponent<TMP_Text>();
|
|
journalSo.ApplyModifiedPropertiesWithoutUndo();
|
|
|
|
var charBtn = CreateTabButton("Character", tabBarGo.transform);
|
|
var skillsBtn = CreateTabButton("Skills", tabBarGo.transform);
|
|
var journalBtn = CreateTabButton("Journal", tabBarGo.transform);
|
|
|
|
var tabBar = tabBarGo.GetComponent<MenuTabBar>();
|
|
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<Button>(), characterPanel.GetComponent<CanvasGroup>(), charView, "Refresh");
|
|
SetTabEntry(tabsProp.GetArrayElementAtIndex(1), skillsBtn.GetComponent<Button>(), skillsPanel.GetComponent<CanvasGroup>(), skillsView, "Refresh");
|
|
SetTabEntry(tabsProp.GetArrayElementAtIndex(2), journalBtn.GetComponent<Button>(), journalPanel.GetComponent<CanvasGroup>(), null);
|
|
tabSo.ApplyModifiedPropertiesWithoutUndo();
|
|
|
|
var menuController = canvasGo.AddComponent<PlayerMenuController>();
|
|
var menuSo = new SerializedObject(menuController);
|
|
menuSo.FindProperty("menuRoot").objectReferenceValue = canvasGo;
|
|
menuSo.FindProperty("tabBar").objectReferenceValue = tabBar;
|
|
menuSo.ApplyModifiedPropertiesWithoutUndo();
|
|
|
|
rootGroup.alpha = 0f;
|
|
rootGroup.interactable = false;
|
|
rootGroup.blocksRaycasts = false;
|
|
|
|
var prefab = PrefabUtility.SaveAsPrefabAsset(canvasGo, MenuCanvasPrefabPath);
|
|
Object.DestroyImmediate(canvasGo);
|
|
return prefab;
|
|
}
|
|
|
|
static void SetTabEntry(SerializedProperty entry, Button button, CanvasGroup panel, Object target, string method = null)
|
|
{
|
|
entry.FindPropertyRelative("button").objectReferenceValue = button;
|
|
entry.FindPropertyRelative("panel").objectReferenceValue = panel;
|
|
if (target != null && method != null)
|
|
{
|
|
var onShown = entry.FindPropertyRelative("onShown");
|
|
onShown.FindPropertyRelative("m_PersistentCalls.m_Calls").arraySize = 1;
|
|
var call = onShown.FindPropertyRelative("m_PersistentCalls.m_Calls").GetArrayElementAtIndex(0);
|
|
call.FindPropertyRelative("m_Target").objectReferenceValue = target;
|
|
call.FindPropertyRelative("m_MethodName").stringValue = method;
|
|
call.FindPropertyRelative("m_Mode").enumValueIndex = 1; // Void
|
|
call.FindPropertyRelative("m_CallState").enumValueIndex = 2; // RuntimeOnly
|
|
}
|
|
}
|
|
|
|
static void WireScene(GameObject streamCanvasPrefab, GameObject hudCanvasPrefab, GameObject menuCanvasPrefab)
|
|
{
|
|
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
|
|
|
|
// UI System root
|
|
var uiSystem = GameObject.Find("UI System");
|
|
if (uiSystem == null)
|
|
{
|
|
uiSystem = new GameObject("UI System");
|
|
}
|
|
|
|
if (uiSystem.GetComponent<UIStateController>() == null)
|
|
{
|
|
uiSystem.AddComponent<UIStateController>();
|
|
}
|
|
|
|
// Player components
|
|
var player = GameObject.Find("PlayerArmature");
|
|
if (player == null)
|
|
{
|
|
Debug.LogError("[UILayerSetup] PlayerArmature not found in scene.");
|
|
return;
|
|
}
|
|
|
|
var controlLock = player.GetComponent<PlayerControlLock>();
|
|
if (controlLock == null)
|
|
{
|
|
controlLock = player.AddComponent<PlayerControlLock>();
|
|
}
|
|
|
|
var menuInput = player.GetComponent<PlayerMenuInput>();
|
|
if (menuInput == null)
|
|
{
|
|
menuInput = player.AddComponent<PlayerMenuInput>();
|
|
}
|
|
|
|
var inputs = player.GetComponent<StarterAssetsInputs>();
|
|
if (inputs != null)
|
|
{
|
|
inputs.cursorLocked = false;
|
|
EditorUtility.SetDirty(inputs);
|
|
}
|
|
|
|
// DialogueTransitionController
|
|
var transition = Object.FindFirstObjectByType<DialogueTransitionController>();
|
|
if (transition != null)
|
|
{
|
|
var tSo = new SerializedObject(transition);
|
|
tSo.FindProperty("controlLock").objectReferenceValue = controlLock;
|
|
tSo.ApplyModifiedPropertiesWithoutUndo();
|
|
}
|
|
|
|
// Instantiate / refresh UI prefab instances
|
|
var streamInstance = EnsurePrefabInstance(streamCanvasPrefab, "DialogueStreamCanvas");
|
|
var hudInstance = EnsurePrefabInstance(hudCanvasPrefab, "HudCanvas");
|
|
var menuInstance = EnsurePrefabInstance(menuCanvasPrefab, "PlayerMenuCanvas");
|
|
|
|
// Wire menu controller refs
|
|
var menuController = menuInstance.GetComponent<PlayerMenuController>();
|
|
if (menuController != null)
|
|
{
|
|
var mSo = new SerializedObject(menuController);
|
|
mSo.FindProperty("menuInput").objectReferenceValue = menuInput;
|
|
mSo.FindProperty("controlLock").objectReferenceValue = controlLock;
|
|
mSo.ApplyModifiedPropertiesWithoutUndo();
|
|
}
|
|
|
|
// Portrait → Open menu
|
|
var portraitButton = hudInstance.GetComponentInChildren<Button>(true);
|
|
if (portraitButton != null && menuController != null)
|
|
{
|
|
while (portraitButton.onClick.GetPersistentEventCount() > 0)
|
|
{
|
|
UnityEditor.Events.UnityEventTools.RemovePersistentListener(portraitButton.onClick, 0);
|
|
}
|
|
|
|
UnityEditor.Events.UnityEventTools.AddPersistentListener(portraitButton.onClick, menuController.Open);
|
|
EditorUtility.SetDirty(portraitButton);
|
|
}
|
|
|
|
// Camera framing
|
|
var vcam = GameObject.Find("CinemachineCamera");
|
|
if (vcam != null)
|
|
{
|
|
var framing = vcam.GetComponent<DialogueCameraFraming>();
|
|
if (framing == null)
|
|
{
|
|
framing = vcam.AddComponent<DialogueCameraFraming>();
|
|
}
|
|
|
|
var composer = vcam.GetComponent<CinemachinePositionComposer>();
|
|
var fSo = new SerializedObject(framing);
|
|
fSo.FindProperty("composer").objectReferenceValue = composer;
|
|
fSo.FindProperty("dialogueScreenX").floatValue = -0.17f;
|
|
fSo.ApplyModifiedPropertiesWithoutUndo();
|
|
}
|
|
|
|
// Re-point DialogueRunner presenters
|
|
var runner = Object.FindFirstObjectByType<DialogueRunner>();
|
|
var streamPresenter = streamInstance.GetComponent<DialogueStreamPresenter>();
|
|
var optionsPresenter = streamInstance.GetComponent<DialogueStreamOptionsPresenter>();
|
|
var lineAdvancer = Object.FindFirstObjectByType<LineAdvancer>();
|
|
|
|
if (runner != null && streamPresenter != null && optionsPresenter != null)
|
|
{
|
|
var rSo = new SerializedObject(runner);
|
|
var presenters = rSo.FindProperty("dialoguePresenters");
|
|
if (presenters != null)
|
|
{
|
|
presenters.arraySize = lineAdvancer != null ? 3 : 2;
|
|
presenters.GetArrayElementAtIndex(0).objectReferenceValue = streamPresenter;
|
|
presenters.GetArrayElementAtIndex(1).objectReferenceValue = optionsPresenter;
|
|
if (lineAdvancer != null)
|
|
{
|
|
presenters.GetArrayElementAtIndex(2).objectReferenceValue = lineAdvancer;
|
|
}
|
|
|
|
rSo.ApplyModifiedPropertiesWithoutUndo();
|
|
}
|
|
}
|
|
|
|
if (lineAdvancer != null && streamPresenter != null)
|
|
{
|
|
var aSo = new SerializedObject(lineAdvancer);
|
|
aSo.FindProperty("presenter").objectReferenceValue = streamPresenter;
|
|
aSo.ApplyModifiedPropertiesWithoutUndo();
|
|
}
|
|
|
|
// Point DialogueTransitionController.dialogueUI at the new canvas
|
|
if (transition != null && streamInstance != null)
|
|
{
|
|
var tSo = new SerializedObject(transition);
|
|
tSo.FindProperty("dialogueUI").objectReferenceValue = streamInstance;
|
|
tSo.FindProperty("controlLock").objectReferenceValue = controlLock;
|
|
tSo.ApplyModifiedPropertiesWithoutUndo();
|
|
}
|
|
|
|
// Deactivate the package Dialogue System Canvas (keep EventSystem alive)
|
|
var dialogueSystem = GameObject.Find("Dialogue System");
|
|
if (dialogueSystem != null)
|
|
{
|
|
var packageCanvas = dialogueSystem.transform.Find("Canvas");
|
|
if (packageCanvas != null)
|
|
{
|
|
// Rescue the Skill Check Banner onto the scene-owned Skill Check
|
|
// Canvas — NOT into streamInstance. EnsurePrefabInstance
|
|
// DestroyImmediates the stream canvas on every re-run, so
|
|
// anything parented under it dies with it; that is what left
|
|
// SkillRuntime.resultView null after the second setup run.
|
|
// Skill Check Canvas is scene-owned, survives re-runs, and its
|
|
// sortingOrder 100 already draws above the dialogue canvas.
|
|
var banner = packageCanvas.Find("Skill Check Banner");
|
|
if (banner != null)
|
|
{
|
|
var bannerHost = GameObject.Find("Skill Check Canvas");
|
|
if (bannerHost != null)
|
|
{
|
|
banner.SetParent(bannerHost.transform, false);
|
|
}
|
|
}
|
|
|
|
packageCanvas.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
|
|
// Repair SkillRuntime.resultView. It can be left dangling by an earlier
|
|
// run of this menu item, and a null here silently disables every roll
|
|
// banner ("SkillCheckResultView is not assigned; skipping roll banner").
|
|
// Prefer an active view if more than one survives.
|
|
SkillCheckResultView resultView = null;
|
|
foreach (var candidate in Object.FindObjectsByType<SkillCheckResultView>(
|
|
FindObjectsInactive.Include, FindObjectsSortMode.None))
|
|
{
|
|
if (resultView == null
|
|
|| (!resultView.gameObject.activeInHierarchy && candidate.gameObject.activeInHierarchy))
|
|
{
|
|
resultView = candidate;
|
|
}
|
|
}
|
|
|
|
var skillRuntime = Object.FindFirstObjectByType<SkillRuntime>();
|
|
if (skillRuntime != null && resultView != null)
|
|
{
|
|
var skillSo = new SerializedObject(skillRuntime);
|
|
skillSo.FindProperty("resultView").objectReferenceValue = resultView;
|
|
skillSo.ApplyModifiedPropertiesWithoutUndo();
|
|
}
|
|
else if (skillRuntime != null)
|
|
{
|
|
Debug.LogWarning("[UILayerSetup] No SkillCheckResultView found in the scene; roll banners will not display.", skillRuntime);
|
|
}
|
|
|
|
// Update NPCStandIn dialogueUI refs to the new canvas
|
|
foreach (var npc in Object.FindObjectsByType<NPCStandIn>(FindObjectsInactive.Include, FindObjectsSortMode.None))
|
|
{
|
|
var so = new SerializedObject(npc);
|
|
var uiProp = so.FindProperty("dialogueUI");
|
|
if (uiProp != null)
|
|
{
|
|
uiProp.objectReferenceValue = streamInstance;
|
|
so.ApplyModifiedPropertiesWithoutUndo();
|
|
}
|
|
}
|
|
|
|
EditorSceneManager.MarkSceneDirty(scene);
|
|
EditorSceneManager.SaveScene(scene);
|
|
}
|
|
|
|
static GameObject EnsurePrefabInstance(GameObject prefab, string name)
|
|
{
|
|
var existing = GameObject.Find(name);
|
|
if (existing != null)
|
|
{
|
|
Object.DestroyImmediate(existing);
|
|
}
|
|
|
|
var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
|
|
instance.name = name;
|
|
return instance;
|
|
}
|
|
|
|
static GameObject CreateCanvas(string name, int sortingOrder)
|
|
{
|
|
var go = new GameObject(name, typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
|
|
var canvas = go.GetComponent<Canvas>();
|
|
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
canvas.sortingOrder = sortingOrder;
|
|
var scaler = go.GetComponent<CanvasScaler>();
|
|
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
|
scaler.referenceResolution = new Vector2(1920f, 1080f);
|
|
scaler.matchWidthOrHeight = 0.5f;
|
|
return go;
|
|
}
|
|
|
|
static GameObject CreateTabPanel(string name, Transform parent)
|
|
{
|
|
var go = new GameObject(name, typeof(RectTransform), typeof(CanvasGroup));
|
|
go.transform.SetParent(parent, false);
|
|
Stretch(go.GetComponent<RectTransform>());
|
|
var group = go.GetComponent<CanvasGroup>();
|
|
group.alpha = 0f;
|
|
group.interactable = false;
|
|
group.blocksRaycasts = false;
|
|
return go;
|
|
}
|
|
|
|
static GameObject CreateTabButton(string label, Transform parent)
|
|
{
|
|
var go = new GameObject(label, typeof(RectTransform), typeof(Image), typeof(Button), typeof(LayoutElement));
|
|
go.transform.SetParent(parent, false);
|
|
go.GetComponent<Image>().color = new Color(0.2f, 0.2f, 0.25f, 1f);
|
|
var text = CreateTmp("Label", go.transform, 22f, FontStyles.Bold, TextAlignmentOptions.Center);
|
|
Stretch(text.GetComponent<RectTransform>());
|
|
Object.DestroyImmediate(text.GetComponent<LayoutElement>());
|
|
text.GetComponent<TMP_Text>().text = label;
|
|
return go;
|
|
}
|
|
|
|
static GameObject CreateTmp(string name, Transform parent, float size, FontStyles style, TextAlignmentOptions align)
|
|
{
|
|
var go = new GameObject(name, typeof(RectTransform), typeof(TextMeshProUGUI), typeof(LayoutElement));
|
|
go.transform.SetParent(parent, false);
|
|
var tmp = go.GetComponent<TextMeshProUGUI>();
|
|
tmp.fontSize = size;
|
|
tmp.fontStyle = style;
|
|
tmp.alignment = align;
|
|
tmp.color = Color.white;
|
|
tmp.text = name;
|
|
tmp.textWrappingMode = TextWrappingModes.Normal;
|
|
|
|
// Deliberately no LayoutElement.preferredHeight. A fixed height here is
|
|
// one line's worth, and LayoutElement outranks TMP's own ILayoutElement
|
|
// report — so a wrapped 4-line body would be allocated ~40px while
|
|
// rendering ~150px, and consecutive rows draw on top of each other.
|
|
// Leaving it at the -1 default lets TMP measure the wrapped text.
|
|
// The component stays so callers can still set preferredWidth /
|
|
// flexibleWidth (see the skill row).
|
|
return go;
|
|
}
|
|
|
|
static GameObject CreateUiImage(string name, Transform parent, Color color)
|
|
{
|
|
var go = new GameObject(name, typeof(RectTransform), typeof(Image), typeof(LayoutElement));
|
|
go.transform.SetParent(parent, false);
|
|
go.GetComponent<Image>().color = color;
|
|
return go;
|
|
}
|
|
|
|
static void Stretch(RectTransform rt, float pad = 0f)
|
|
{
|
|
rt.anchorMin = Vector2.zero;
|
|
rt.anchorMax = Vector2.one;
|
|
rt.offsetMin = new Vector2(pad, pad);
|
|
rt.offsetMax = new Vector2(-pad, -pad);
|
|
}
|
|
|
|
static void ConfigureVerticalContent(GameObject go)
|
|
{
|
|
var rt = go.GetComponent<RectTransform>();
|
|
rt.anchorMin = new Vector2(0f, 0f);
|
|
rt.anchorMax = new Vector2(1f, 0f);
|
|
rt.pivot = new Vector2(0.5f, 0f);
|
|
|
|
// A RectTransform added via `new GameObject(..., typeof(RectTransform))`
|
|
// starts at sizeDelta (100,100). With horizontal-stretch anchors that
|
|
// makes the rect parentWidth + 100 wide, centred — overhanging 50px off
|
|
// each side, and the RectMask2D shears the left edge off every label.
|
|
// ContentSizeFitter only governs the vertical axis, so it never
|
|
// corrects this. Zero the horizontal size explicitly.
|
|
rt.sizeDelta = new Vector2(0f, rt.sizeDelta.y);
|
|
rt.anchoredPosition = Vector2.zero;
|
|
|
|
var vlg = go.GetComponent<VerticalLayoutGroup>();
|
|
vlg.childControlHeight = true;
|
|
vlg.childControlWidth = true;
|
|
vlg.childForceExpandHeight = false;
|
|
vlg.childForceExpandWidth = true;
|
|
vlg.spacing = 4f;
|
|
var csf = go.GetComponent<ContentSizeFitter>();
|
|
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
}
|
|
}
|