인물정보, 소지품 분리

This commit is contained in:
2026-07-31 11:51:50 +09:00
parent 23e93a67f8
commit 4dc22eb32b
36 changed files with 598 additions and 120 deletions

View File

@@ -2,7 +2,7 @@
using UnityEngine;
using UnityEngine.UIElements;
// "어떤 대화를 걸지" 고르는 메뉴 (역전재판식 "대화하기" — 후보가 여럿이면 골라서 시작).
// "어떤 대화를 걸지" 고르는 메뉴 — 한 캐릭터에게 걸 수 있는 대화가 여럿이면 골라서 시작한다.
// DialogEnterUI.uxml의 ListView로 후보를 띄우고, 행을 클릭하면 그 인덱스를 반환한다.
// 대사 도중의 분기 선택지(ChoiceHud)와는 별개다 — 이건 대화를 시작하기 전 단계의 UI.
// DialogPlayer.SelectBeat가 재생 가능한 비트가 여럿일 때 사용한다.

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 0c3122efb3a7a304ca03b2bfaa0fad18
guid: 026166c0bdc41234c8ea34d015e7ce9f
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -4,24 +4,36 @@
using UnityEngine.Events;
using UnityEngine.UIElements;
// 증거품창 (역전재판의 법정기록). ItemListUI.uxml을 띄우고 가진 아이템을 좌우 화살표로 넘겨 본다.
// 증거품창. 가진 항목을 좌우 화살표로 넘겨 본다.
//
// 제시(들이대기): 대화 노드가 히든 분기로 무장돼 있는 동안 아이템 그림을 클릭하면
// HiddenBranchResolver.Fire(아이템 Id)를 쏜다 — DialogTouchZone의 부위 키와 완전히 같은 통로라서,
// 노드에 Key = 아이템 Id 인 히든 분기를 달아두면 그 분기가 열린다. 발동하면 창은 자동으로 닫힌다.
// 무장돼 있지 않으면(평소) 클릭해도 아무 일도 없다 — 그냥 열람용 창이다.
// 하위 분류가 둘이고 Evidence 버튼 하나로 순환한다: 닫힘 → 소지품 → 인물 정보 → 닫힘.
// 두 분류는 같은 창·같은 레이아웃을 쓰고, 보고 있는 분류의 항목만 목록에 오른다.
//
// 목록은 StoryManager의 아이템 이력(Id)을 ItemDatabase로 복원해 만든다. 획득 순서 = 표시 순서.
// 제시(들이대기): 대화 노드가 히든 분기로 무장돼 있는 동안 그림을 클릭하면
// HiddenBranchResolver.Fire(항목 Id)를 쏜다 — DialogTouchZone의 부위 키와 완전히 같은 통로라서,
// 노드에 Key = 항목 Id 인 히든 분기를 달아두면 그 분기가 열린다. 발동하면 창은 자동으로 닫힌다.
// 소지품과 인물 모두 제시할 수 있다. 무장돼 있지 않으면(평소) 클릭해도 아무 일도 없다.
//
// 목록은 StoryManager의 획득 이력(Id)을 EvidenceDatabase로 복원해 만든다. 획득 순서 = 표시 순서.
[RequireComponent(typeof(PanelRenderer))]
public class ItemHud : MonoBehaviour
public class EvidenceHud : MonoBehaviour
{
public static ItemHud Instance { get; private set; }
public static EvidenceHud Instance { get; private set; }
[Tooltip("Id → ItemData 복원용 아이템 전체 목록 (게임에 하나)")]
[SerializeField] private ItemDatabase _database;
[Tooltip("Id → EvidenceData 복원용 전체 목록 (소지품·인물 모두 포함, 게임에 하나)")]
[SerializeField] private EvidenceDatabase _database;
[Tooltip("증거품이 하나도 없을 때 설명 칸에 표시할 문구")]
[SerializeField] private string _emptyText = "가지고 있는 증거품이 없습니다.";
[Tooltip("창 상단에 표시할 소지품 분류 이름")]
[SerializeField] private string _itemCategoryLabel = "소지품";
[Tooltip("창 상단에 표시할 인물 정보 분류 이름")]
[SerializeField] private string _profileCategoryLabel = "인물 정보";
[Tooltip("가진 소지품이 없을 때 설명 칸에 표시할 문구")]
[SerializeField] private string _emptyItemText = "가지고 있는 소지품이 없습니다.";
[Tooltip("등록된 인물이 없을 때 설명 칸에 표시할 문구")]
[SerializeField] private string _emptyProfileText = "등록된 인물이 없습니다.";
[Tooltip("제시가 실제로 발동했을 때 호출 (연출·사운드용). 발동하지 않은 클릭에는 불리지 않는다")]
[SerializeField] private UnityEvent _onPresented;
@@ -35,7 +47,8 @@ public class ItemHud : MonoBehaviour
private PanelRenderer _panelRenderer;
private VisualElement _root; // 전체 토글 대상(#Body)
private VisualElement _itemNode; // 슬라이드 대상 (.ItemNode — 그림+설명 묶음)
private VisualElement _itemNode; // 슬라이드 대상 (.EvidenceNode — 그림+설명 묶음)
private Label _categoryLabel; // 창 상단 분류 이름 (#EvidenceCategory)
private Image _image;
private Label _label;
@@ -48,8 +61,9 @@ public class ItemHud : MonoBehaviour
// 현재 표시 상태. 리로드로 요소가 새로 만들어져도 이 값으로 복원된다.
private bool _visible;
private EvidenceCategory _category = EvidenceCategory.Item; // 지금 보고 있는 분류
private int _index;
private readonly List<ItemData> _owned = new();
private readonly List<EvidenceData> _owned = new();
// 창이 떠 있는가 (DialogTouchZone이 창 뒤 월드 클릭을 무시하는 데 쓴다)
public bool IsOpen => _visible;
@@ -100,7 +114,8 @@ private void OnUIReload(PanelRenderer panelRenderer, VisualElement root, int ver
_uiVersion = version;
_root = root.Q<VisualElement>("Body");
_itemNode = root.Q(className: "ItemNode");
_itemNode = root.Q(className: "EvidenceNode");
_categoryLabel = root.Q<Label>("EvidenceCategory");
_image = root.Q<Image>("Image");
_label = root.Q<Label>("Label");
@@ -112,11 +127,28 @@ private void OnUIReload(PanelRenderer panelRenderer, VisualElement root, int ver
}
// ── 공개 API (버튼 UnityEvent 연결용) ────────────────────────
public void Toggle() { if (_visible) Hide(); else Show(); }
public void Show()
// Evidence 버튼: 닫힘 → 소지품 → 인물 정보 → 닫힘 순으로 순환한다.
// 한 버튼으로 열기·분류 전환·닫기를 다 처리한다.
public void Toggle()
{
if (!_visible)
Show(EvidenceCategory.Item); // 닫힘 → 소지품
else if (_category == EvidenceCategory.Item)
Show(EvidenceCategory.Profile); // 소지품 → 인물 정보
else
Hide(); // 인물 정보 → 닫힘
}
public void Show() => Show(EvidenceCategory.Item);
public void Show(EvidenceCategory category)
{
// 분류가 바뀌면 목록 자체가 달라지므로 첫 항목부터 다시 본다
if (!_visible || _category != category) _index = 0;
_visible = true;
_category = category;
_slideGen++; // 진행 중이던 슬라이드 취소
ResetSlideStyle();
ApplyState();
@@ -131,7 +163,7 @@ public void Hide()
}
// ── 내부 ─────────────────────────────────────────────────────
private ItemData CurrentItem =>
private EvidenceData CurrentItem =>
_owned.Count > 0 && _index >= 0 && _index < _owned.Count ? _owned[_index] : null;
private void Step(int delta)
@@ -228,17 +260,23 @@ private void OnStoryChanged()
if (_visible) ApplyState();
}
// 가진 항목 중 "지금 보고 있는 분류"만 추린다. 획득 순서 = 표시 순서.
private void RebuildOwned()
{
_owned.Clear();
var story = StoryManager.Instance;
if (story == null || _database == null) return;
foreach (var id in story.ItemIds)
foreach (var id in story.EvidenceIds)
{
var item = _database.FindById(id);
if (item != null) _owned.Add(item);
else Debug.LogWarning($"[ItemHud] ItemDatabase에 없는 아이템 Id: {id}");
// 분류는 항목이 아니라 DB의 어느 목록에 들어 있는지가 정한다
var entry = _database.FindById(id, out var category);
if (entry == null)
{
Debug.LogWarning($"[EvidenceHud] EvidenceDatabase에 없는 Id: {id}");
continue;
}
if (category == _category) _owned.Add(entry);
}
}
@@ -248,17 +286,22 @@ private void ApplyState()
RebuildOwned();
if (_index >= _owned.Count) _index = Mathf.Max(0, _owned.Count - 1);
var item = CurrentItem;
// 분류 이름은 항목 유무와 무관하게 항상 지금 보고 있는 분류를 가리킨다
if (_categoryLabel != null)
_categoryLabel.text = _category == EvidenceCategory.Item
? _itemCategoryLabel : _profileCategoryLabel;
var entry = CurrentItem;
if (_image != null)
{
_image.sprite = item != null ? item.Icon : null;
_image.style.visibility = item != null ? Visibility.Visible : Visibility.Hidden;
_image.sprite = entry != null ? entry.Icon : null;
_image.style.visibility = entry != null ? Visibility.Visible : Visibility.Hidden;
}
if (_label != null)
{
_label.text = item != null
? $"<b>{item.Name}</b>\n\n{item.Description}"
: _emptyText;
_label.text = entry != null
? $"<b>{entry.Name}</b>\n\n{entry.Description}"
: (_category == EvidenceCategory.Item ? _emptyItemText : _emptyProfileText);
}
if (_root != null)