Files
2026-07-31 11:51:50 +09:00

198 lines
9.0 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
// 이야기 상태(StoryState)의 유일한 접근 창구.
// 진행도/호감도/이력의 조회·변경과 저장/로드를 담당한다.
// 씬에 미리 배치하지 않아도 첫 접근 때 자동 생성된다.
public class StoryManager : MonoBehaviour
{
public static StoryManager Instance { get; private set; }
[Tooltip("플레이 시작 시 메인 진행도 초기값 — 씬을 직접 열어 테스트할 때 그 씬 기준 진행도로 시작하게. " +
"이 매니저가 살아남는 씬(플레이를 시작한 씬)의 값만 적용되고, 씬 전환으로 넘어온 경우엔 무시된다")]
[Min(0)] [SerializeField] private int _initialMainProgress;
[Serializable]
private struct InitialAffection
{
public CharacterData Character;
public int Amount;
}
[Tooltip("플레이 시작 시 호감도 초기값 — 이 씬만 직접 열어 테스트할 때 앞 과정 없이 원하는 호감도로 시작하게. " +
"진행도 초기값과 마찬가지로 플레이를 시작한 씬의 값만 적용되고, 씬 전환으로 넘어온 경우엔 무시된다")]
[SerializeField] private List<InitialAffection> _initialAffections = new();
// 아래 두 목록은 찾기 쉽게 나눠 둔 것일 뿐, 항목의 실제 분류는 EvidenceDatabase의
// 어느 목록에 등록했는지가 정한다. 여기서 자리를 바꿔도 창에 뜨는 분류는 달라지지 않는다.
[Tooltip("플레이 시작 시 들고 있을 소지품 — 이 씬만 직접 열어 테스트할 때 앞 과정 없이 시작하게. " +
"다른 초기값과 마찬가지로 플레이를 시작한 씬의 값만 적용되고, 씬 전환으로 넘어온 경우엔 무시된다. " +
"실제 게임에서의 획득은 AddEvidence(터치존·버튼 UnityEvent 등)으로")]
[SerializeField] private List<EvidenceData> _initialItems = new();
[Tooltip("플레이 시작 시 열려 있을 인물 정보 — 위 소지품과 동작은 같다")]
[SerializeField] private List<EvidenceData> _initialProfiles = new();
private StoryState _state = new();
// 상태가 바뀔 때마다 발행 (호감도 게이지 등 UI 갱신용)
public event Action Changed;
private void Awake()
{
if (Instance == null)
{
Instance = this; //만들어진 자신을 인스턴스로 설정
_state.MainProgress = _initialMainProgress; // 테스트용 초기 진행도 (씬 전환으로 넘어온 중복은 여기 못 옴)
// 테스트용 초기 호감도 (첫 인스턴스에서만 — 씬 전환으로 넘어온 경우엔 이 자리에 못 옴)
foreach (var init in _initialAffections)
if (init.Character != null)
_state.Affection[IdOf(init.Character)] = init.Amount;
// 테스트용 초기 소지 증거품 항목 (두 목록 모두 같은 획득 이력으로 들어간다)
AddInitialEvidence(_initialItems);
AddInitialEvidence(_initialProfiles);
}
else
{
Destroy(gameObject); //이미 인스턴스가 있으면 자신을 파괴
}
}
private void AddInitialEvidence(List<EvidenceData> list)
{
foreach (var entry in list)
if (entry != null && !_state.Evidence.Contains(EvidenceIdOf(entry)))
_state.Evidence.Add(EvidenceIdOf(entry));
}
// ── 메인 진행도 ──────────────────────────────────────────────
public int MainProgress
{
get => _state.MainProgress;
set
{
if (_state.MainProgress == value) return;
_state.MainProgress = value;
Changed?.Invoke();
}
}
// UnityEvent 연결용 (버튼·이벤트존·노드 이벤트 등에서 진행도 조작)
public void AddMainProgress(int amount) => MainProgress += amount;
public void SetMainProgress(int value) => MainProgress = value;
// ── 호감도 ──────────────────────────────────────────────────
public int GetAffection(CharacterData character)
=> character != null && _state.Affection.TryGetValue(IdOf(character), out var v) ? v : 0;
public void AddAffection(CharacterData character, int delta)
{
if (character == null || delta == 0) return;
_state.Affection[IdOf(character)] = GetAffection(character) + delta;
Changed?.Invoke();
}
// CharacterData의 Id를 키로 사용 (비어있으면 에셋 이름)
private static string IdOf(CharacterData c) => string.IsNullOrEmpty(c.Id) ? c.name : c.Id;
// ── 대화 이력 ────────────────────────────────────────────────
public bool IsDialogCompleted(string groupName) => _state.CompletedDialogs.Contains(groupName);
// 완료 기록. 처음 완료한 경우에만 true (진행도 보상 중복 방지용)
public bool MarkDialogCompleted(string groupName)
{
if (string.IsNullOrEmpty(groupName) || !_state.CompletedDialogs.Add(groupName))
return false;
Changed?.Invoke();
return true;
}
// ── 트리거 ──────────────────────────────────────────────────
public bool HasTrigger(string id) => _state.Triggers.Contains(id);
// UnityEvent 연결용 (버튼·이벤트 등에서 트리거 켜기). 대화 조건 RequiredTriggerIds에서 검사한다.
public void SetTrigger(string id)
{
if (string.IsNullOrEmpty(id) || !_state.Triggers.Add(id)) return;
Changed?.Invoke();
}
// ── 증거품 (소지품 · 인물) ───────────────────────────────────
public IReadOnlyList<string> EvidenceIds => _state.Evidence;
public bool HasEvidence(EvidenceData entry) => entry != null && _state.Evidence.Contains(EvidenceIdOf(entry));
// UnityEvent 연결용 (대화 터치존·월드 버튼 등에서 증거품 항목 획득). 중복 획득은 무시된다.
public void AddEvidence(EvidenceData entry)
{
if (entry == null) return;
string id = EvidenceIdOf(entry);
if (_state.Evidence.Contains(id)) return;
_state.Evidence.Add(id);
Changed?.Invoke();
}
public void RemoveEvidence(EvidenceData entry)
{
if (entry == null || !_state.Evidence.Remove(EvidenceIdOf(entry))) return;
Changed?.Invoke();
}
// EvidenceData의 Id를 키로 사용 (비어있으면 에셋 이름) — 캐릭터 IdOf와 같은 규칙
private static string EvidenceIdOf(EvidenceData e) => string.IsNullOrEmpty(e.Id) ? e.name : e.Id;
// ── 선택 이력 ────────────────────────────────────────────────
public bool HasChosen(string code) => _state.ChosenCodes.Contains(code);
public void RecordChoice(string code)
{
if (string.IsNullOrEmpty(code) || !_state.ChosenCodes.Add(code)) return;
Changed?.Invoke();
}
// ── 저장 / 로드 ──────────────────────────────────────────────
// 플레이 시작 시엔 항상 빈 상태로 시작한다 (테스트 반복이 꼬이지 않게).
// 이어하기를 만들 때 타이틀 화면 등에서 Load()를 호출하면 된다.
private static string SavePath => Path.Combine(Application.persistentDataPath, "story_state.json");
public void Save() => File.WriteAllText(SavePath, _state.ToJson());
public void CallLoad() => Load();
// 저장 파일이 있으면 불러온다. 성공 여부 반환.
public bool Load()
{
if (!File.Exists(SavePath))
{
Debug.LogWarning("세이브 파일이 없습니다.");
return false;
}
try
{
var loaded = StoryState.FromJson(File.ReadAllText(SavePath));
if (loaded == null)
{
Debug.LogWarning("세이브 파일이 잘못되었습니다.");
return false;
}
_state = loaded;
Changed?.Invoke();
return true;
}
catch (Exception e)
{
Debug.LogWarning($"[StoryManager] 저장 파일 로드 실패: {e.Message}");
return false;
}
}
// Enter Play Mode에서 도메인 리로드를 꺼도 이전 인스턴스 참조가 안 남게
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetStatics() => Instance = null;
}