using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; // "어떤 대화를 걸지" 고르는 메뉴 (역전재판식 "대화하기" — 후보가 여럿이면 골라서 시작). // DialogEnterUI.uxml의 ListView로 후보를 띄우고, 행을 클릭하면 그 인덱스를 반환한다. // 대사 도중의 분기 선택지(ChoiceHud)와는 별개다 — 이건 대화를 시작하기 전 단계의 UI. // DialogPlayer.SelectBeat가 재생 가능한 비트가 여럿일 때 사용한다. // // UIDocument의 후속인 PanelRenderer를 쓴다. 요소 참조는 UI 리로드 콜백으로 받는다: // - rootVisualElement가 준비됐는지 매번 확인하던 지연 초기화(EnsureRefs)가 사라진다. // - 리로드되면 ListView 인스턴스 자체가 새로 만들어지므로, 콜백에서 매번 다시 연결한다. [RequireComponent(typeof(PanelRenderer))] public class DialogEnterHud : MonoBehaviour { public static DialogEnterHud Instance { get; private set; } private PanelRenderer _panelRenderer; private VisualElement _root; // 전체 토글 대상(#Body) private ListView _listView; // 같은 버전으로 콜백이 중복 호출될 때 헛일을 막는다 (Unity 권장 패턴) private int _uiVersion = -1; private List _options; private AwaitableCompletionSource _completion; // 현재 표시 상태. 리로드로 요소가 새로 만들어졌을 때 이 값으로 복원한다 // (메뉴를 띄운 채 리로드돼도 대기 중인 Show가 그대로 살아 있다). private bool _visible; private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; _panelRenderer = GetComponent(); // root가 이미 준비돼 있으면 즉시 호출되고, 이후 UI가 리로드될 때마다 다시 호출된다 _panelRenderer.RegisterUIReloadCallback(OnUIReload); } private void OnDestroy() { if (_panelRenderer != null) _panelRenderer.UnregisterUIReloadCallback(OnUIReload); if (Instance == this) Instance = null; } private void OnDisable() { // 씬 전환 등으로 비활성화될 때 진행 중 대기 정리 _completion?.TrySetCanceled(); _completion = null; } // UI가 (재)구성될 때마다 요소를 다시 잡고 ListView 콜백을 걸고 현재 상태를 되돌린다. private void OnUIReload(PanelRenderer panelRenderer, VisualElement root, int version) { if (_uiVersion == version) return; _uiVersion = version; _root = root.Q("Body"); _listView = root.Q(); if (_listView != null) { _listView.selectionType = SelectionType.Single; _listView.bindItem = BindRow; // item-template(DialogEnterRow)이 makeItem을 담당, 바인딩만 우리가 _listView.selectionChanged += OnSelectionChanged; } ApplyState(); // 첫 호출에선 _visible=false라 숨김 상태로 시작한다 } // 대기 중인 선택을 취소하고 메뉴를 닫는다 (다른 NPC와 대화를 새로 시작할 때 등). // 취소된 쪽의 await Show(...)는 OperationCanceledException으로 빠져나간다. public void CancelPending() => _completion?.TrySetCanceled(); // 후보 목록을 띄우고 고른 인덱스를 반환한다. 취소되면 OperationCanceledException. public async Awaitable Show(List options) { if (options == null || options.Count == 0) return 0; if (_root == null || _listView == null) return 0; // UI가 아직 준비되지 않음 // 이전 호출이 아직 대기 중이면 먼저 취소한다 (마지막 호출이 이긴다). CancelPending(); _options = options; _visible = true; ApplyState(); var completion = new AwaitableCompletionSource(); _completion = completion; int result; try { result = await completion.Awaitable; } finally { // 내가 아직 현재 세션일 때만 정리 — 취소 직후 다른 NPC가 새로 띄운 메뉴를 지우면 안 된다 if (_completion == completion) { _completion = null; Hide(); } } return result; } // ListView 행(item-template = DialogEnterRow.uxml)에 후보 이름을 채운다. // 행 템플릿의 Label엔 name이 없어 타입으로 찾는다. private void BindRow(VisualElement element, int i) { var label = element.Q