diff --git a/CLAUDE.md b/CLAUDE.md index efc2887..60bbf36 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -328,10 +328,14 @@ There is **no manual sync step** for dialogue. The chain is: The only things that need a human/Editor step are the *data* files, not the dialogue: ``` -skill_bible.json → Tools ▸ Nightclub Arcadia ▸ (SkillSystemSetup) → Skills/Definitions/*.asset + SkillDatabase.asset -candidates.json → Tools ▸ Nightclub Arcadia ▸ (CandidateSystemSetup) → Candidates/Definitions/*.asset + CandidateDatabase.asset +skill_bible.json → Tools ▸ Nightclub Arcadia ▸ Set Up Skill System → Skills/Definitions/*.asset + SkillDatabase.asset +candidates.json → Tools ▸ Nightclub Arcadia ▸ Set Up Candidate System → Candidates/Definitions/*.asset + CandidateDatabase.asset +characters.json → Tools ▸ Nightclub Arcadia ▸ Set Up Characters → Characters/Definitions/*.asset ``` +`characters.json` is where an NPC or a talkable object gets its display name, its Yarn node and +its interaction range. Adding one is a JSON entry plus a prefab; it needs no code. + Those setup scripts also register a `[DidReloadScripts]` callback, so in practice the assets regenerate on script reload too. The JSON is the source of truth either way. @@ -445,8 +449,8 @@ should not be — see `docs/restructure-plan.md`. - Namespaces: `NightclubArcadia.` — `Skills`, `Dialogue`, `UI`, `UI.Dialogue`, `UI.HUD`, `UI.Menu`, `Cinematics`, `Interaction`, `Player`, `Player.Math`. - **Exception:** `NPCStandIn.cs` sits in the global namespace. That is a scaffold tell, not a - convention to copy — new runtime types always get a namespace. + There are no global-namespace runtime types left; the last one, `NPCStandIn`, went with the + scaffold, and `InputDeviceTracker` moved into `NightclubArcadia.Core`. - **Assemblies.** No project code is in `Assembly-CSharp` any more: | Assembly | Covers | Depends on | @@ -457,6 +461,10 @@ should not be — see `docs/restructure-plan.md`. | `NightclubArcadia.Game` | the rest of `Scripts` | Core, Skills, Math, StarterAssets, packages | | `StarterAssets` | vendored template code | Input System | + `Scripts/Interaction` holds the interaction layer: `InteractableDefinition` (the asset that + says what a thing is called and which node it starts), `DialogueInteractor` (the component + that places one in the world), plus `WorldSpaceLabel` and `InteractionPrompt`. + `Game` is one assembly rather than one per area because the areas are genuinely cyclic: UI ↔ Dialogue, UI ↔ Player and UI ↔ Camera, via `UILayerBootstrap`, `PlayerControlLock` and `CharacterPanelView`. Splitting further means moving those three, not just adding asmdefs. @@ -505,15 +513,13 @@ should not be — see `docs/restructure-plan.md`. four runtime files use — without that, everything referencing it would have stopped compiling. Keep it in mind before adding loose scripts outside an asmdef folder. -5. **The one-shot scene builders under `Assets/Editor` are disabled, on purpose.** - `Set Up UI Layer`, `Set Up Player Control & Camera` and `Set Up Yarn Sandbox Scene` were - written for the single-scene layout. After the split they do damage rather than work: pointed - at a level they rebuild systems content that already exists in `Systems.unity`, and pointed at - Systems, `PlayerControlSetup` drags navigation in and silently turns that scene binary (trap 1 - again). Both were observed. They now return early via `LegacySceneSetup.Blocked` and are kept - only as a record of how the wiring works until prefabs replace them (Phase 5). - `Set Up Skill System` and `Set Up Candidate System` still work — they regenerate assets from - JSON and touch no scene. **To run the game, open `Bootstrap.unity` and press Play.** +5. **The one-shot scene builders are gone.** `UILayerSetup`, `PlayerControlSetup`, + `UILayerSetupAutoRun` and `YarnDemoSetup` built scenes from scratch and assumed the + single-scene layout; after the split they produced duplicates, and one of them destroyed a + level. The prefabs they used to generate are authored and committed now, so they were deleted + rather than fixed. They are in git history if the wiring ever needs consulting. + `Set Up Skill System`, `Set Up Candidate System` and `Set Up Characters` remain — they + regenerate assets from JSON and touch no scene. **To run the game, open `Bootstrap.unity`.** 6. **`unity run` reserves `-batchmode`, `-nographics`, `-quit`, `-logFile`.** Passing any of them after `--` is a hard error, and its output does not reach stdout — read `Logs/Editor.log` (§3.4). diff --git a/NightclubArcadia/Assets/Scripts/NPCs.meta b/NightclubArcadia/Assets/Characters.meta similarity index 77% rename from NightclubArcadia/Assets/Scripts/NPCs.meta rename to NightclubArcadia/Assets/Characters.meta index 043b7d4..85fe6dc 100644 --- a/NightclubArcadia/Assets/Scripts/NPCs.meta +++ b/NightclubArcadia/Assets/Characters.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 2216657f3a0224501bcfa09add14f689 +guid: fd96ed0121314a93aff9d3ef7845fd1d folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/NightclubArcadia/Assets/Characters/Definitions.meta b/NightclubArcadia/Assets/Characters/Definitions.meta new file mode 100644 index 0000000..c2a52fc --- /dev/null +++ b/NightclubArcadia/Assets/Characters/Definitions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 642dac0a511114afa9f701fb8a59378c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Characters/Definitions/bartender.asset b/NightclubArcadia/Assets/Characters/Definitions/bartender.asset new file mode 100644 index 0000000..c3bba35 --- /dev/null +++ b/NightclubArcadia/Assets/Characters/Definitions/bartender.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8f4df63859ea4e39910d900aa7a5510a, type: 3} + m_Name: bartender + m_EditorClassIdentifier: NightclubArcadia.Game::NightclubArcadia.Interaction.InteractableDefinition + id: bartender + displayName: Bartender + yarnNode: Bartender_Talk + interactionRange: 3 + showLabel: 1 diff --git a/NightclubArcadia/Assets/Characters/Definitions/bartender.asset.meta b/NightclubArcadia/Assets/Characters/Definitions/bartender.asset.meta new file mode 100644 index 0000000..72bd4e0 --- /dev/null +++ b/NightclubArcadia/Assets/Characters/Definitions/bartender.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8ccb543c858f74a969cb025cb3e1f0ea +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Characters/Definitions/chair.asset b/NightclubArcadia/Assets/Characters/Definitions/chair.asset new file mode 100644 index 0000000..8b901e1 --- /dev/null +++ b/NightclubArcadia/Assets/Characters/Definitions/chair.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8f4df63859ea4e39910d900aa7a5510a, type: 3} + m_Name: chair + m_EditorClassIdentifier: NightclubArcadia.Game::NightclubArcadia.Interaction.InteractableDefinition + id: chair + displayName: Chair + yarnNode: Chair_Interaction + interactionRange: 3 + showLabel: 1 diff --git a/NightclubArcadia/Assets/Characters/Definitions/chair.asset.meta b/NightclubArcadia/Assets/Characters/Definitions/chair.asset.meta new file mode 100644 index 0000000..0dee580 --- /dev/null +++ b/NightclubArcadia/Assets/Characters/Definitions/chair.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 403e142c457b0412a8c8604a2a9807ed +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Characters/characters.json b/NightclubArcadia/Assets/Characters/characters.json new file mode 100644 index 0000000..c0ced8d --- /dev/null +++ b/NightclubArcadia/Assets/Characters/characters.json @@ -0,0 +1,18 @@ +{ + "interactables": [ + { + "id": "bartender", + "displayName": "Bartender", + "yarnNode": "Bartender_Talk", + "interactionRange": 3.0, + "showLabel": true + }, + { + "id": "chair", + "displayName": "Chair", + "yarnNode": "Chair_Interaction", + "interactionRange": 3.0, + "showLabel": true + } + ] +} diff --git a/NightclubArcadia/Assets/Characters/characters.json.meta b/NightclubArcadia/Assets/Characters/characters.json.meta new file mode 100644 index 0000000..78fbfb6 --- /dev/null +++ b/NightclubArcadia/Assets/Characters/characters.json.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b4673507aac641739818bc04a57a8930 +TextScriptImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Editor/CharacterSetup.cs b/NightclubArcadia/Assets/Editor/CharacterSetup.cs new file mode 100644 index 0000000..9e11430 --- /dev/null +++ b/NightclubArcadia/Assets/Editor/CharacterSetup.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NightclubArcadia.Interaction; +using UnityEditor; +using UnityEditor.Callbacks; +using UnityEngine; + +namespace NightclubArcadia.EditorTools +{ + /// + /// Regenerates InteractableDefinition assets from Assets/Characters/characters.json. + /// + /// The JSON is the source of truth and the .asset files are build output; hand edits + /// to them are overwritten. Same contract as Skills and Candidates, deliberately — + /// a writer adding a character edits one JSON file and never opens Unity. + /// + /// This exists because the identity of an NPC used to live in public fields on a + /// scene object, where nothing outside that scene could see it and two copies could + /// disagree. They did: the scene's second "Bartender" was the chair. + /// + public static class CharacterSetup + { + const string JsonPath = "Assets/Characters/characters.json"; + const string DefinitionsFolder = "Assets/Characters/Definitions"; + + [Serializable] + class Dto + { + public string id; + public string displayName; + public string yarnNode; + public float interactionRange = 3f; + public bool showLabel = true; + } + + [Serializable] + class DtoList + { + public Dto[] interactables; + } + + [MenuItem("Tools/Nightclub Arcadia/Set Up Characters")] + public static void SetUpCharacters() => Regenerate(verbose: true); + + [DidReloadScripts] + static void OnScriptsReloaded() => Regenerate(verbose: false); + + static void Regenerate(bool verbose) + { + if (!File.Exists(JsonPath)) + { + if (verbose) + { + Debug.LogError($"[Characters] Missing {JsonPath}"); + } + return; + } + + DtoList parsed; + try + { + parsed = JsonUtility.FromJson(File.ReadAllText(JsonPath)); + } + catch (Exception e) + { + Debug.LogError($"[Characters] {JsonPath} is not valid JSON: {e.Message}"); + return; + } + + if (parsed?.interactables == null || parsed.interactables.Length == 0) + { + Debug.LogError($"[Characters] {JsonPath} defines no interactables."); + return; + } + + var duplicates = parsed.interactables.GroupBy(i => i.id).Where(g => g.Count() > 1).ToList(); + if (duplicates.Count > 0) + { + Debug.LogError("[Characters] Duplicate ids: " + + string.Join(", ", duplicates.Select(g => g.Key))); + return; + } + + if (!AssetDatabase.IsValidFolder(DefinitionsFolder)) + { + AssetDatabase.CreateFolder("Assets/Characters", "Definitions"); + } + + var live = new HashSet(StringComparer.Ordinal); + var changed = new List(); + + foreach (var dto in parsed.interactables) + { + if (string.IsNullOrWhiteSpace(dto.id)) + { + Debug.LogError("[Characters] An entry has no id; skipped."); + continue; + } + live.Add(dto.id); + + var path = $"{DefinitionsFolder}/{dto.id}.asset"; + var asset = AssetDatabase.LoadAssetAtPath(path); + if (asset == null) + { + asset = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(asset, path); + changed.Add(dto.id); + } + else if (asset.DisplayName != dto.displayName || asset.YarnNode != dto.yarnNode || + !Mathf.Approximately(asset.InteractionRange, dto.interactionRange) || + asset.ShowLabel != dto.showLabel) + { + changed.Add(dto.id); + } + + asset.EditorSet(dto.id, dto.displayName, dto.yarnNode, dto.interactionRange, dto.showLabel); + EditorUtility.SetDirty(asset); + } + + // An id removed from the JSON should not leave an orphan asset behind that a + // prefab could still be pointing at. + foreach (var guid in AssetDatabase.FindAssets("t:InteractableDefinition", new[] { DefinitionsFolder })) + { + var path = AssetDatabase.GUIDToAssetPath(guid); + var name = Path.GetFileNameWithoutExtension(path); + if (!live.Contains(name)) + { + Debug.LogWarning($"[Characters] Removing orphan definition '{name}' — no longer in the JSON."); + AssetDatabase.DeleteAsset(path); + } + } + + AssetDatabase.SaveAssets(); + + if (verbose || changed.Count > 0) + { + Debug.Log($"[Characters] {parsed.interactables.Length} definition(s) in {DefinitionsFolder}" + + (changed.Count > 0 ? $"; updated: {string.Join(", ", changed)}" : "; no changes")); + } + } + } +} diff --git a/NightclubArcadia/Assets/Editor/UILayerSetup.cs.meta b/NightclubArcadia/Assets/Editor/CharacterSetup.cs.meta similarity index 83% rename from NightclubArcadia/Assets/Editor/UILayerSetup.cs.meta rename to NightclubArcadia/Assets/Editor/CharacterSetup.cs.meta index c69db78..0fc0951 100644 --- a/NightclubArcadia/Assets/Editor/UILayerSetup.cs.meta +++ b/NightclubArcadia/Assets/Editor/CharacterSetup.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 5bdf044f1ab04312b76895dac1740b37 +guid: 9cbb2b742dcc4031a4e0cfc0c0544faf MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/NightclubArcadia/Assets/Editor/LegacySceneSetup.cs b/NightclubArcadia/Assets/Editor/LegacySceneSetup.cs deleted file mode 100644 index fec48e7..0000000 --- a/NightclubArcadia/Assets/Editor/LegacySceneSetup.cs +++ /dev/null @@ -1,48 +0,0 @@ -using UnityEditor; -using UnityEngine; - -namespace NightclubArcadia.EditorTools -{ - /// - /// Gate for the one-shot scene builders under Assets/Editor. - /// - /// Those menus were written when the whole game lived in a single scene. They find - /// or create the dialogue system, the UI canvases, the player and the camera rig, - /// and save the result into one scene. After the split into Bootstrap / Systems / - /// Level, that assumption is false, and running them does damage rather than work: - /// - /// - Pointed at a level, they rebuild systems content that already exists in - /// Systems.unity, leaving duplicate UIStateControllers and dialogue runners. - /// - Pointed at Systems, PlayerControlSetup drags navigation in with it. The - /// baked NavMeshData is embedded rather than an asset, and NavMeshData prefers - /// binary serialization, so the whole scene silently turns binary — the exact - /// problem that took a bisection to find the first time (CLAUDE.md trap 1). - /// - /// Both were observed, not theorised. So the builders are blocked rather than - /// retargeted, and deliberately not deleted: UILayerSetup alone is 818 lines that - /// still record how the UI layer is wired, and that record is worth keeping until - /// the prefabs replace it (docs/restructure-plan.md Phase 5). - /// - /// Asset generators are unaffected. Regenerating SkillDefinitions or the candidate - /// registry from JSON touches no scene and stays available. - /// - public static class LegacySceneSetup - { - /// - /// Call at the top of any menu that builds scene content. Returns true if the - /// caller should stop, which is currently always. - /// - public static bool Blocked(string menuName, string whatItWouldDo) - { - Debug.LogError( - $"'{menuName}' is disabled.\n\n" + - $"It was written for the single-scene layout and would {whatItWouldDo}. " + - "The game is now split across Bootstrap, Systems and a level scene, and the " + - "objects it builds already exist in Assets/Scenes/Systems/Systems.unity.\n\n" + - "To run the game: open Assets/Scenes/Bootstrap.unity and press Play.\n" + - "To verify it comes up: make test-play\n" + - "Rewriting these for the split is docs/restructure-plan.md Phase 5."); - return true; - } - } -} diff --git a/NightclubArcadia/Assets/Editor/PlayerControlSetup.cs b/NightclubArcadia/Assets/Editor/PlayerControlSetup.cs deleted file mode 100644 index 56bd4b2..0000000 --- a/NightclubArcadia/Assets/Editor/PlayerControlSetup.cs +++ /dev/null @@ -1,320 +0,0 @@ -using NightclubArcadia.Cinematics; -using NightclubArcadia.Interaction; -using NightclubArcadia.Player; -using StarterAssets; -using Unity.AI.Navigation; -using Unity.Cinemachine; -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEngine; -using UnityEngine.AI; - -/// -/// Wires player control, navigation, and camera systems into Systems.unity. -/// Run via Tools → Nightclub Arcadia → Set Up Player Control & Camera. -/// -public static class PlayerControlSetup -{ - const string ScenePath = NightclubArcadia.EditorTools.ScenePaths.Systems; - const string PlayerPrefabPath = "Assets/StarterAssets/ThirdPersonController/Prefabs/PlayerArmature.prefab"; - const string SettingsFolder = "Assets/Settings"; - - [MenuItem("Tools/Nightclub Arcadia/Set Up Player Control & Camera")] - public static void SetUp() - { - if (NightclubArcadia.EditorTools.LegacySceneSetup.Blocked("Set Up Player Control & Camera", - "rebuild the player and camera rig and pull navigation into Systems.unity, " - + "which turns that scene binary")) return; - - EnsurePlayerPrefab(); - WireScene(); - AssetDatabase.SaveAssets(); - Debug.Log("[PlayerControlSetup] Player control and camera setup complete."); - } - - static void EnsurePlayerPrefab() - { - var prefab = AssetDatabase.LoadAssetAtPath(PlayerPrefabPath); - if (prefab == null) - { - Debug.LogError($"[PlayerControlSetup] Missing prefab at {PlayerPrefabPath}"); - return; - } - - var root = PrefabUtility.LoadPrefabContents(PlayerPrefabPath); - - EnsureComponent(root); - EnsureComponent(root); - EnsureComponent(root); - EnsureComponent(root); - EnsureComponent(root); - - var inputs = root.GetComponent(); - if (inputs != null) - { - inputs.analogMovement = true; - inputs.cursorLocked = false; - } - - var controller = root.GetComponent(); - if (controller != null) - { - controller.LockCameraPosition = true; - } - - var clickToMove = root.GetComponent(); - var pointerNav = root.GetComponent(); - var pointerInput = root.GetComponent(); - var arbiter = root.GetComponent(); - - WireSerializedReference(clickToMove, "brainCamera", null); - WireSerializedReference(pointerNav, "pointerInput", pointerInput); - WireSerializedReference(pointerNav, "clickToMove", clickToMove); - WireSerializedReference(pointerNav, "motionArbiter", arbiter); - - PrefabUtility.SaveAsPrefabAsset(root, PlayerPrefabPath); - PrefabUtility.UnloadPrefabContents(root); - } - - static void WireScene() - { - var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single); - var player = GameObject.FindGameObjectWithTag("Player"); - if (player == null) - { - Debug.LogError("[PlayerControlSetup] No Player-tagged object found in scene."); - return; - } - - EnsureComponent(player); - EnsureComponent(player); - EnsureComponent(player); - EnsureComponent(player); - EnsureComponent(player); - - var inputs = player.GetComponent(); - if (inputs != null) - { - inputs.analogMovement = true; - inputs.cursorLocked = false; - } - - var controller = player.GetComponent(); - if (controller != null) - { - controller.LockCameraPosition = true; - } - - var brainCamera = Camera.main; - var pointerInput = player.GetComponent(); - var clickToMove = player.GetComponent(); - var pointerNav = player.GetComponent(); - var arbiter = player.GetComponent(); - - WireSerializedReference(clickToMove, "brainCamera", brainCamera); - WireSerializedReference(pointerNav, "brainCamera", brainCamera); - WireSerializedReference(pointerNav, "pointerInput", pointerInput); - WireSerializedReference(pointerNav, "clickToMove", clickToMove); - WireSerializedReference(pointerNav, "motionArbiter", arbiter); - - var groundLayer = LayerMask.NameToLayer("Ground"); - var interactableLayer = LayerMask.NameToLayer("Interactable"); - SetLayerIfExists("Floor", groundLayer); - SetLayerIfExists("Room_101", groundLayer); - SetLayerIfExists("Wall_Back", groundLayer); - SetLayerIfExists("Wall_Front", groundLayer); - SetLayerIfExists("Table", groundLayer); - - WireBartender("Bartender", interactableLayer); - WireBartender("Bartender (1)", interactableLayer); - - EnsureNavigation(player.transform.position); - EnsureCameraAnchor(player.transform, brainCamera); - - EditorSceneManager.MarkSceneDirty(scene); - EditorSceneManager.SaveScene(scene); - } - - static void WireBartender(string objectName, int interactableLayer) - { - var bartender = GameObject.Find(objectName); - if (bartender == null) - { - return; - } - - if (interactableLayer >= 0) - { - bartender.layer = interactableLayer; - } - - EnsureComponent(bartender); - EnsureComponent(bartender); - } - - static void EnsureNavigation(Vector3 sampleOrigin) - { - var navigation = GameObject.Find("Navigation"); - if (navigation == null) - { - navigation = new GameObject("Navigation"); - } - - var surface = EnsureComponent(navigation); - surface.agentTypeID = 0; - surface.collectObjects = CollectObjects.All; - surface.useGeometry = NavMeshCollectGeometry.RenderMeshes; - - var included = 0; - var defaultLayer = LayerMask.NameToLayer("Default"); - var groundLayer = LayerMask.NameToLayer("Ground"); - if (defaultLayer >= 0) - { - included |= 1 << defaultLayer; - } - - if (groundLayer >= 0) - { - included |= 1 << groundLayer; - } - - surface.layerMask = included; - - var table = GameObject.Find("Table"); - if (table != null) - { - var modifier = EnsureComponent(table); - modifier.overrideArea = true; - modifier.area = NavMesh.GetAreaFromName("Not Walkable"); - } - - surface.BuildNavMesh(); - - if (!NavMesh.SamplePosition(sampleOrigin, out _, 2f, NavMesh.AllAreas)) - { - Debug.LogWarning("[PlayerControlSetup] NavMesh bake did not cover the player start position."); - } - } - - static void EnsureCameraAnchor(Transform followTarget, Camera brainCamera) - { - var vcamObject = GameObject.Find("CinemachineCamera"); - if (vcamObject == null) - { - Debug.LogError("[PlayerControlSetup] CinemachineCamera not found."); - return; - } - - var vcam = vcamObject.GetComponent(); - var composer = vcamObject.GetComponent(); - - var anchorObject = GameObject.Find("CameraAnchor"); - if (anchorObject == null) - { - anchorObject = new GameObject("CameraAnchor"); - } - - anchorObject.transform.position = followTarget.position; - - var anchor = EnsureComponent(anchorObject); - EnsureComponent(anchorObject); - EnsureComponent(anchorObject); - EnsureComponent(anchorObject); - - WireSerializedReference(anchor, "followTarget", followTarget); - WireSerializedReference(anchor, "composer", composer); - - if (vcam != null) - { - vcam.Target.TrackingTarget = anchorObject.transform; - vcam.Priority.Value = 10; - } - - var pan = anchorObject.GetComponent(); - WireSerializedReference(pan, "brainCamera", brainCamera); - WireSerializedReference(pan, "anchor", anchor); - WireSerializedReference(pan, "stateDirector", anchorObject.GetComponent()); - - var poiDirector = anchorObject.GetComponent(); - WireSerializedReference(poiDirector, "anchor", anchor); - WireSerializedReference(poiDirector, "stateDirector", anchorObject.GetComponent()); - - var stateDirector = anchorObject.GetComponent(); - WireSerializedReference(stateDirector, "anchor", anchor); - - EnsureRevealCamera(vcamObject, followTarget); - } - - static void EnsureRevealCamera(GameObject followVcam, Transform followTarget) - { - var revealName = "CM_Reveal_Room101"; - var revealObject = GameObject.Find(revealName); - if (revealObject == null) - { - revealObject = new GameObject(revealName); - revealObject.transform.SetPositionAndRotation( - followVcam.transform.position + new Vector3(-4f, 2f, -2f), - followVcam.transform.rotation); - } - - var revealCam = EnsureComponent(revealObject); - revealCam.Target.TrackingTarget = followTarget; - revealCam.Priority.Value = 0; - revealCam.Lens.FieldOfView = followVcam.GetComponent().Lens.FieldOfView; - - var volumeObject = GameObject.Find("RevealVolume_Room101"); - if (volumeObject == null) - { - volumeObject = new GameObject("RevealVolume_Room101"); - volumeObject.transform.position = followTarget.position + new Vector3(0f, 1f, 2f); - } - - var box = EnsureComponent(volumeObject); - box.isTrigger = true; - box.size = new Vector3(6f, 3f, 4f); - - var volume = EnsureComponent(volumeObject); - WireSerializedReference(volume, "revealCamera", revealCam); - } - - static T EnsureComponent(GameObject target) where T : Component - { - var component = target.GetComponent(); - if (component == null) - { - component = target.AddComponent(); - } - - return component; - } - - static void SetLayerIfExists(string objectName, int layer) - { - if (layer < 0) - { - return; - } - - var obj = GameObject.Find(objectName); - if (obj != null) - { - obj.layer = layer; - } - } - - static void WireSerializedReference(Object target, string propertyName, Object value) - { - if (target == null) - { - return; - } - - var so = new SerializedObject(target); - var property = so.FindProperty(propertyName); - if (property != null) - { - property.objectReferenceValue = value; - so.ApplyModifiedPropertiesWithoutUndo(); - } - } -} diff --git a/NightclubArcadia/Assets/Editor/PlayerControlSetup.cs.meta b/NightclubArcadia/Assets/Editor/PlayerControlSetup.cs.meta deleted file mode 100644 index 6ca74c5..0000000 --- a/NightclubArcadia/Assets/Editor/PlayerControlSetup.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: eedfabcda58c94099b2ad3e19f141b09 \ No newline at end of file diff --git a/NightclubArcadia/Assets/Editor/ScenePaths.cs b/NightclubArcadia/Assets/Editor/ScenePaths.cs index 5fc7cc1..de9f3ca 100644 --- a/NightclubArcadia/Assets/Editor/ScenePaths.cs +++ b/NightclubArcadia/Assets/Editor/ScenePaths.cs @@ -15,30 +15,8 @@ namespace NightclubArcadia.EditorTools /// Persistent systems: dialogue, skills, UI, camera rig, player. public const string Systems = "Assets/Scenes/Systems/Systems.unity"; - /// - /// The scene the setup menus wire into. SC-101's conference hall. - /// - /// Only for menus that OPEN and MODIFY a scene. A menu that builds a scene from - /// scratch and saves over its target must never point here — see Sandbox. - /// + /// The level scene. SC-101's conference hall. public const string WorkingLevel = "Assets/Scenes/Levels/SC101_ConferenceHall.unity"; - /// - /// Scratch scene for menus that generate a scene wholesale. - /// - /// This exists because pointing such a menu at WorkingLevel destroyed it once: - /// YarnDemoSetup builds a demo scene and calls SaveScene over its target, so - /// running it replaced SC-101 with a bare Yarn demo. Anything under Scenes/Dev - /// is disposable by definition; nothing outside it is. - /// - public const string Sandbox = "Assets/Scenes/Dev/YarnSandbox.unity"; - - /// - /// Guard for any menu that overwrites a whole scene. Generated scenes belong in - /// Scenes/Dev and nowhere else. - /// - public static bool IsDisposable(string scenePath) => - !string.IsNullOrEmpty(scenePath) && - scenePath.Replace('\\', '/').StartsWith("Assets/Scenes/Dev/", System.StringComparison.Ordinal); } } diff --git a/NightclubArcadia/Assets/Editor/UILayerSetup.cs b/NightclubArcadia/Assets/Editor/UILayerSetup.cs deleted file mode 100644 index 8286007..0000000 --- a/NightclubArcadia/Assets/Editor/UILayerSetup.cs +++ /dev/null @@ -1,821 +0,0 @@ -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 Systems.unity. -/// Run via Tools → Nightclub Arcadia → Set Up UI Layer. -/// -public static class UILayerSetup -{ - const string ScenePath = NightclubArcadia.EditorTools.ScenePaths.Systems; - 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() - { - if (NightclubArcadia.EditorTools.LegacySceneSetup.Blocked("Set Up UI Layer", - "rebuild the UI canvases that already live in Systems.unity, leaving duplicates")) return; - - 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