From 9a0618fd312ba5715bd6d7ce39f3c61281076ec7 Mon Sep 17 00:00:00 2001 From: grabowskil Date: Tue, 25 Aug 2026 21:53:06 +0200 Subject: [PATCH] replace the NPC scaffold with data-driven prefabs, and retire the builders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NPCStandIn was written to be dropped on a sphere and carried its identity in per-instance public fields. The cost of that showed up in the scene itself: of the two spheres, one was the Bartender and the other was named "Bartender (1)" in the hierarchy while being configured as the Chair — data changed, name never was. Nothing outside the scene could see what existed, and two copies could disagree. Identity now lives in an InteractableDefinition asset and the scene only places a prefab. The migration matched the old objects by yarn node rather than by name, which is the only reason the chair ended up as the chair. InteractableDefinition rather than the planned "character definition": it covers a character and a talkable object equally, so Bartender.prefab sits under Prefabs/Characters and Chair.prefab under Prefabs/Interactables. Three components collapse into one. DialogueInteractable was in no scene at all — a dead trigger-zone variant of the same idea. ClickInteractableBridge existed only to adapt NPCStandIn without modifying it, a constraint that died with NPCStandIn; DialogueInteractor implements IClickInteractable itself. The parts worth keeping were lifted out rather than discarded: WorldSpaceLabel and InteractionPrompt are now normal components on prefab children, so a label can be seen and positioned in the Editor instead of existing only at runtime. InputDeviceTracker moved into NightclubArcadia.Core and gained a namespace, leaving no global-namespace runtime types. Characters get the same pipeline as Skills and Candidates: Assets/Characters/characters.json is the source of truth, CharacterSetup regenerates the definition assets on script reload and removes orphans. Adding an NPC is a JSON entry plus a prefab — no code, and no Unity for the data half. Phase 5.4 deletes the one-shot builders rather than disabling them. UILayerSetup, PlayerControlSetup, UILayerSetupAutoRun and YarnDemoSetup assumed the single-scene layout, had already destroyed a level scene and turned Systems binary once each, and after NPCStandIn went they no longer compiled. The prefabs they used to generate are authored and committed; the wiring knowledge is in those prefabs and in git history. The asset generators — Skill, Candidate, Character — are untouched and still work. EditMode 42/42, PlayMode 7/7, YarnCheck 9 files / 35 nodes. All three scenes still text. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 32 +- .../{Scripts/NPCs.meta => Characters.meta} | 2 +- .../Assets/Characters/Definitions.meta | 8 + .../Characters/Definitions/bartender.asset | 19 + .../Definitions/bartender.asset.meta | 8 + .../Assets/Characters/Definitions/chair.asset | 19 + .../Characters/Definitions/chair.asset.meta | 8 + .../Assets/Characters/characters.json | 18 + .../Assets/Characters/characters.json.meta | 11 + .../Assets/Editor/CharacterSetup.cs | 144 +++ ...erSetup.cs.meta => CharacterSetup.cs.meta} | 2 +- .../Assets/Editor/LegacySceneSetup.cs | 48 - .../Assets/Editor/PlayerControlSetup.cs | 320 ------- .../Assets/Editor/PlayerControlSetup.cs.meta | 2 - NightclubArcadia/Assets/Editor/ScenePaths.cs | 24 +- .../Assets/Editor/UILayerSetup.cs | 821 ------------------ .../Assets/Editor/UILayerSetupAutoRun.cs | 48 - .../Assets/Editor/YarnDemoSetup.cs | 120 --- .../Assets/Editor/YarnDemoSetup.cs.meta | 2 - .../Assets/Prefabs/Characters.meta | 8 + .../Prefabs/Characters/Bartender.prefab | 539 ++++++++++++ .../Prefabs/Characters/Bartender.prefab.meta | 7 + .../Characters/Bartender_Placeholder.mat | 137 +++ .../Characters/Bartender_Placeholder.mat.meta | 8 + .../Assets/Prefabs/Interactables.meta | 8 + .../Assets/Prefabs/Interactables/Chair.prefab | 539 ++++++++++++ .../Prefabs/Interactables/Chair.prefab.meta | 7 + .../Interactables/Chair_Placeholder.mat | 137 +++ .../Interactables/Chair_Placeholder.mat.meta | 8 + .../Scenes/Levels/SC101_ConferenceHall.unity | 476 +++------- .../Assets/Scripts/Core/InputDeviceTracker.cs | 61 ++ .../{NPCs => Core}/InputDeviceTracker.cs.meta | 0 .../Scripts/Core/NightclubArcadia.Core.asmdef | 4 +- .../Scripts/Dialogue/DialogueInteractable.cs | 93 -- .../Dialogue/DialogueInteractable.cs.meta | 2 - .../Dialogue/DialogueTransitionController.cs | 2 +- .../Scripts/Dialogue/DialogueUIVisibility.cs | 2 +- .../Interaction/ClickInteractableBridge.cs | 29 - .../ClickInteractableBridge.cs.meta | 2 - .../Scripts/Interaction/DialogueInteractor.cs | 170 ++++ .../Interaction/DialogueInteractor.cs.meta} | 2 +- .../Interaction/InteractableDefinition.cs | 46 + .../InteractableDefinition.cs.meta} | 2 +- .../Scripts/Interaction/InteractionPrompt.cs | 54 ++ .../Interaction/InteractionPrompt.cs.meta | 11 + .../Scripts/Interaction/WorldSpaceLabel.cs | 54 ++ .../Interaction/WorldSpaceLabel.cs.meta | 11 + .../Assets/Scripts/NPCs/InputDeviceTracker.cs | 48 - .../Assets/Scripts/NPCs/NPCStandIn.cs | 234 ----- .../Assets/Scripts/NPCs/NPCStandIn.cs.meta | 2 - .../Assets/Scripts/UI/UILayerBootstrap.cs | 6 +- .../Assets/Tests/PlayMode/BootstrapTests.cs | 17 +- .../Assets/Tests/PlayMode/SceneOrderTests.cs | 55 +- docs/restructure-plan.md | 50 +- 54 files changed, 2292 insertions(+), 2195 deletions(-) rename NightclubArcadia/Assets/{Scripts/NPCs.meta => Characters.meta} (77%) create mode 100644 NightclubArcadia/Assets/Characters/Definitions.meta create mode 100644 NightclubArcadia/Assets/Characters/Definitions/bartender.asset create mode 100644 NightclubArcadia/Assets/Characters/Definitions/bartender.asset.meta create mode 100644 NightclubArcadia/Assets/Characters/Definitions/chair.asset create mode 100644 NightclubArcadia/Assets/Characters/Definitions/chair.asset.meta create mode 100644 NightclubArcadia/Assets/Characters/characters.json create mode 100644 NightclubArcadia/Assets/Characters/characters.json.meta create mode 100644 NightclubArcadia/Assets/Editor/CharacterSetup.cs rename NightclubArcadia/Assets/Editor/{UILayerSetup.cs.meta => CharacterSetup.cs.meta} (83%) delete mode 100644 NightclubArcadia/Assets/Editor/LegacySceneSetup.cs delete mode 100644 NightclubArcadia/Assets/Editor/PlayerControlSetup.cs delete mode 100644 NightclubArcadia/Assets/Editor/PlayerControlSetup.cs.meta delete mode 100644 NightclubArcadia/Assets/Editor/UILayerSetup.cs delete mode 100644 NightclubArcadia/Assets/Editor/UILayerSetupAutoRun.cs delete mode 100644 NightclubArcadia/Assets/Editor/YarnDemoSetup.cs delete mode 100644 NightclubArcadia/Assets/Editor/YarnDemoSetup.cs.meta create mode 100644 NightclubArcadia/Assets/Prefabs/Characters.meta create mode 100644 NightclubArcadia/Assets/Prefabs/Characters/Bartender.prefab create mode 100644 NightclubArcadia/Assets/Prefabs/Characters/Bartender.prefab.meta create mode 100644 NightclubArcadia/Assets/Prefabs/Characters/Bartender_Placeholder.mat create mode 100644 NightclubArcadia/Assets/Prefabs/Characters/Bartender_Placeholder.mat.meta create mode 100644 NightclubArcadia/Assets/Prefabs/Interactables.meta create mode 100644 NightclubArcadia/Assets/Prefabs/Interactables/Chair.prefab create mode 100644 NightclubArcadia/Assets/Prefabs/Interactables/Chair.prefab.meta create mode 100644 NightclubArcadia/Assets/Prefabs/Interactables/Chair_Placeholder.mat create mode 100644 NightclubArcadia/Assets/Prefabs/Interactables/Chair_Placeholder.mat.meta create mode 100644 NightclubArcadia/Assets/Scripts/Core/InputDeviceTracker.cs rename NightclubArcadia/Assets/Scripts/{NPCs => Core}/InputDeviceTracker.cs.meta (100%) delete mode 100644 NightclubArcadia/Assets/Scripts/Dialogue/DialogueInteractable.cs delete mode 100644 NightclubArcadia/Assets/Scripts/Dialogue/DialogueInteractable.cs.meta delete mode 100644 NightclubArcadia/Assets/Scripts/Interaction/ClickInteractableBridge.cs delete mode 100644 NightclubArcadia/Assets/Scripts/Interaction/ClickInteractableBridge.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/Interaction/DialogueInteractor.cs rename NightclubArcadia/Assets/{Editor/LegacySceneSetup.cs.meta => Scripts/Interaction/DialogueInteractor.cs.meta} (83%) create mode 100644 NightclubArcadia/Assets/Scripts/Interaction/InteractableDefinition.cs rename NightclubArcadia/Assets/{Editor/UILayerSetupAutoRun.cs.meta => Scripts/Interaction/InteractableDefinition.cs.meta} (83%) create mode 100644 NightclubArcadia/Assets/Scripts/Interaction/InteractionPrompt.cs create mode 100644 NightclubArcadia/Assets/Scripts/Interaction/InteractionPrompt.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/Interaction/WorldSpaceLabel.cs create mode 100644 NightclubArcadia/Assets/Scripts/Interaction/WorldSpaceLabel.cs.meta delete mode 100644 NightclubArcadia/Assets/Scripts/NPCs/InputDeviceTracker.cs delete mode 100644 NightclubArcadia/Assets/Scripts/NPCs/NPCStandIn.cs delete mode 100644 NightclubArcadia/Assets/Scripts/NPCs/NPCStandIn.cs.meta 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