This commit is contained in:
2026-07-24 18:20:02 +09:00
commit 0b02bacb3d
177 changed files with 5172 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
using UnityEngine;
// 장소 프리팹 안의 캐릭터 자리 하나.
// 이 캐릭터가 지금 이 장소에서 걸 수 있는 대화(StoryBeat)가 하나라도 있으면 캐릭터를 켜고, 없으면 끈다.
// 갱신은 LocationManager가 장소 입장 시 / 스토리 상태 변화 시 Refresh를 호출해서 이뤄진다.
//
// 배치법: 이 컴포넌트는 항상 켜져 있는 빈 오브젝트에 붙이고,
// 캐릭터 본체(CharacterVoiceObject + DialogPlayer)는 그 자식으로 두고 _character에 연결.
// 참고: 조건 없는 잡담 비트를 하나 등록해 두면 그 캐릭터는 항상 등장한다.
public class CharacterSlot : MonoBehaviour
{
[Tooltip("캐릭터 본체 (CharacterVoiceObject + DialogPlayer가 붙은 자식 오브젝트)")]
[SerializeField] private CharacterVoiceObject _character;
public void Refresh(StoryDatabase database, LocationData location)
{
if (_character == null || _character.Character == null) return;
_character.gameObject.SetActive(database.HasPlayableBeat(location, _character.Character));
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b8df8bed7639e304cbe98206c74e9f13

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8f8b437cb3f4c66489eed95f0c1158a7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,58 @@
using UnityEditor;
using UnityEngine;
// StoryBeat를 리스트에서 접었을 때 "Element N" 대신 요약 제목을 보여준다.
// Note가 있으면 Note 우선, 없으면 "[진행도] 장소 · 캐릭터 · 대화"를 자동 조합.
[CustomPropertyDrawer(typeof(StoryBeat))]
public class StoryBeatDrawer : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
=> EditorGUI.GetPropertyHeight(property, label, includeChildren: true);
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
string summary = BuildSummary(property);
if (!string.IsNullOrEmpty(summary))
label = new GUIContent(summary, label.tooltip);
EditorGUI.PropertyField(position, property, label, includeChildren: true);
}
private static string BuildSummary(SerializedProperty property)
{
string note = property.FindPropertyRelative("Note").stringValue;
if (!string.IsNullOrWhiteSpace(note))
return note;
string location = NameOf(property.FindPropertyRelative("Location").objectReferenceValue);
string character = NameOf(property.FindPropertyRelative("Character").objectReferenceValue);
string group = NameOf(property.FindPropertyRelative("Group").objectReferenceValue);
// 아무것도 안 채워진 새 항목은 기본 라벨(Element N) 유지
if (location == null && character == null && group == null)
return null;
var progress = property.FindPropertyRelative("Condition").FindPropertyRelative("MainProgress");
int min = progress.FindPropertyRelative("Min").intValue;
int max = progress.FindPropertyRelative("Max").intValue;
string range = max >= 9999 ? $"[{min}~]" : $"[{min}~{max}]";
return $"{range} {location ?? "?"} · {character ?? "?"} · {group ?? "?"}";
}
// 에셋별 표시 이름 (지정 안 됐으면 에셋 이름, 그마저 없으면 null)
private static string NameOf(Object obj)
{
switch (obj)
{
case LocationData loc:
return string.IsNullOrWhiteSpace(loc.DisplayName) ? loc.name : loc.DisplayName;
case CharacterData ch:
return string.IsNullOrWhiteSpace(ch.Name) ? ch.name : ch.Name;
case DialogGroup grp:
return string.IsNullOrWhiteSpace(grp.DialogGroupName) ? grp.name : grp.DialogGroupName;
default:
return obj != null ? obj.name : null;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: eade7ff33c9514b479d3f1e4ba2da912

View File

@@ -0,0 +1,14 @@
using UnityEngine;
// 게임 속 장소 하나 (로비, 사무실, 법정 …).
// StoryBeat가 "어느 장소의 대화인지"를 이 에셋으로 가리키고,
// LocationManager가 이동 시 Prefab을 띄운다.
[CreateAssetMenu(menuName = "Story/Location")]
public class LocationData : ScriptableObject
{
[Tooltip("장소 표시 이름 (이동 메뉴 등 UI용)")]
public string DisplayName;
[Tooltip("이 장소의 프리팹 — 배경 + 캐릭터 슬롯(CharacterSlot)들 + 이동 버튼 등")]
public GameObject Prefab;
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e92ba75bdc5f28f4983e164d45c3b27d

View File

@@ -0,0 +1,29 @@
using System;
using UnityEngine;
// 스토리 비트 하나 — "어느 장소에서, 어느 캐릭터가, 어떤 조건일 때, 무슨 대화를 하는가".
// StoryDatabase의 목록이 게임 전체 스토리 흐름의 단일 원본이다.
[Serializable]
public class StoryBeat
{
[Tooltip("에디터 식별용 메모 (게임에는 안 나옴)")]
public string Note;
[Tooltip("이 대화가 일어나는 장소")]
public LocationData Location;
[Tooltip("대화를 거는 캐릭터")]
public CharacterData Character;
[Tooltip("재생할 대화 그래프 (.dlg = DialogGroup)")]
public DialogGroup Group;
[Tooltip("활성 조건 (진행도/호감도 범위 + 트리거)")]
public DialogCondition Condition = new();
[Tooltip("이 대화를 처음 완료하면 메인 진행도 +N (필수 대화가 아니면 0)")]
[Min(0)] public int ProgressOnComplete;
[Tooltip("대화 선택 메뉴에 표시할 이름 (비우면 그룹 이름 사용)")]
public string MenuLabel;
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1b03cd6e1bb5fbb44b82670d620875c3

View File

@@ -0,0 +1,51 @@
using System.Collections.Generic;
using UnityEngine;
// 스토리 비트 전체 목록 — "누가 어디서 언제 무슨 대화를 하는가"의 단일 원본.
// 캐릭터 슬롯 활성화(HasPlayableBeat)와 DialogPlayer의 대화 후보(GetPlayableBeats)가
// 전부 이 에셋을 조회한다. 게임에 하나만 만들어 LocationManager에 꽂아 쓴다.
[CreateAssetMenu(menuName = "Story/Story Database")]
public class StoryDatabase : ScriptableObject
{
[Tooltip("스토리 비트 목록 — 위에 있을수록 대화 선택 메뉴에서 우선순위가 높다. " +
"진행도 순으로 정렬해 두면 이 목록이 곧 시나리오 진행표가 된다")]
[SerializeField] private List<StoryBeat> _beats = new();
public IReadOnlyList<StoryBeat> Beats => _beats;
// 이 장소·캐릭터의 비트 중 지금 조건을 만족하는 것들 (목록 순서 유지)
public List<StoryBeat> GetPlayableBeats(LocationData location, CharacterData character)
{
var result = new List<StoryBeat>();
foreach (var beat in _beats)
if (IsPlayable(beat, location, character))
result.Add(beat);
return result;
}
// 이 장소에서 이 캐릭터가 걸 수 있는 대화가 하나라도 있는가 (캐릭터 슬롯 활성화 판정)
public bool HasPlayableBeat(LocationData location, CharacterData character)
{
foreach (var beat in _beats)
if (IsPlayable(beat, location, character))
return true;
return false;
}
// 강제 재생(DialogPlayer.PlayGroup)용 — 이 그룹이 등록된 첫 비트 (없으면 null)
public StoryBeat FindBeat(DialogGroup group)
{
if (group == null) return null;
foreach (var beat in _beats)
if (beat.Group == group)
return beat;
return null;
}
private static bool IsPlayable(StoryBeat beat, LocationData location, CharacterData character)
{
if (beat.Location != location || beat.Character != character || beat.Group == null)
return false;
return beat.Condition == null || beat.Condition.IsMet(character);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0c7c50927085b814097de120bc52f6c2

View File

@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using UnityEngine;
// 이야기 진행 상태 데이터 (메인 진행도 / 호감도 / 대화 이력 / 선택 이력).
// 상태 보관과 JSON 변환만 담당한다 — 게임 로직에서는 StoryManager를 통해 접근할 것.
public class StoryState
{
public int MainProgress;
public readonly Dictionary<string, int> Affection = new(); // 캐릭터 Id → 호감도
public readonly HashSet<string> CompletedDialogs = new(); // 완료한 DialogGroup 이름
public readonly HashSet<string> ChosenCodes = new(); // 골랐던 선택지 Code
public readonly HashSet<string> Triggers = new(); // 켜진 트리거 Id (대화 조건 검사용)
public void Clear()
{
MainProgress = 0;
Affection.Clear();
CompletedDialogs.Clear();
ChosenCodes.Clear();
Triggers.Clear();
}
// ── JSON 변환 ────────────────────────────────────────────────
// JsonUtility가 Dictionary/HashSet을 직렬화하지 못해 리스트로 바꿔 저장한다.
[Serializable]
private class JsonData
{
public int MainProgress;
public List<string> AffectionIds = new();
public List<int> AffectionValues = new();
public List<string> CompletedDialogs = new();
public List<string> ChosenCodes = new();
public List<string> Triggers = new();
}
public string ToJson()
{
var data = new JsonData { MainProgress = MainProgress };
foreach (var kvp in Affection)
{
data.AffectionIds.Add(kvp.Key);
data.AffectionValues.Add(kvp.Value);
}
data.CompletedDialogs.AddRange(CompletedDialogs);
data.ChosenCodes.AddRange(ChosenCodes);
data.Triggers.AddRange(Triggers);
return JsonUtility.ToJson(data, prettyPrint: true);
}
// json이 유효하지 않으면 null 반환
public static StoryState FromJson(string json)
{
var data = JsonUtility.FromJson<JsonData>(json);
if (data == null) return null;
var state = new StoryState { MainProgress = data.MainProgress };
for (int i = 0; i < data.AffectionIds.Count && i < data.AffectionValues.Count; i++)
state.Affection[data.AffectionIds[i]] = data.AffectionValues[i];
state.CompletedDialogs.UnionWith(data.CompletedDialogs);
state.ChosenCodes.UnionWith(data.ChosenCodes);
state.Triggers.UnionWith(data.Triggers ?? new List<string>());
return state;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d47259c7403ae7e42b37a411170bc21c