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