173 lines
7.2 KiB
C#
173 lines
7.2 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();
|
|
|
|
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;
|
|
}
|
|
else
|
|
{
|
|
Destroy(gameObject); //이미 인스턴스가 있으면 자신을 파괴
|
|
}
|
|
}
|
|
|
|
// ── 메인 진행도 ──────────────────────────────────────────────
|
|
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 HasChosen(string code) => _state.ChosenCodes.Contains(code);
|
|
|
|
public void RecordChoice(string code)
|
|
{
|
|
if (string.IsNullOrEmpty(code) || !_state.ChosenCodes.Add(code)) return;
|
|
Changed?.Invoke();
|
|
}
|
|
|
|
// ── 미션 이력 ────────────────────────────────────────────────
|
|
public bool IsMissionCompleted(string missionId) => _state.CompletedMissions.Contains(missionId);
|
|
|
|
// 완료 기록. 처음 완료한 경우에만 true (진행도 보상 중복 방지용)
|
|
public bool MarkMissionCompleted(string missionId)
|
|
{
|
|
if (string.IsNullOrEmpty(missionId) || !_state.CompletedMissions.Add(missionId))
|
|
return false;
|
|
Changed?.Invoke();
|
|
return true;
|
|
}
|
|
|
|
// ── 이벤트존 이력 ────────────────────────────────────────────
|
|
public bool HasTriggeredZone(string zoneId) => _state.TriggeredZones.Contains(zoneId);
|
|
|
|
public void RecordZoneTriggered(string zoneId)
|
|
{
|
|
if (string.IsNullOrEmpty(zoneId) || !_state.TriggeredZones.Add(zoneId)) 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;
|
|
}
|
|
}
|
|
|
|
// 새 게임용 초기화 (저장 파일은 다음 Save 때 덮어써짐)
|
|
public void ResetAll()
|
|
{
|
|
_state.Clear();
|
|
Changed?.Invoke();
|
|
}
|
|
|
|
// Enter Play Mode에서 도메인 리로드를 꺼도 이전 인스턴스 참조가 안 남게
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
|
private static void ResetStatics() => Instance = null;
|
|
}
|