using System; using System.Collections.Generic; using System.Globalization; using System.IO; using UnityEngine; namespace TKS.OutGame { // 세이브 = 로컬 JSON 1장(기획서 §11 · Application.persistentDataPath/save.json · 버전 필드 · 변경 즉시 저장). // §11 의 「id → 값」 맵은 JsonUtility 가 Dictionary 를 못 읽어 「id 필드를 가진 배열」로 둔다(필드 이름·뜻은 §11 그대로). [Serializable] public class LevelState { public string id; public int lv = 1; public int exp; } [Serializable] public class HeroState : LevelState { public string grade = ""; [Tooltip("승급 장비 6칸(장착한 gear id · 빈 칸 = \"\")")] public List gear = new List(); [Tooltip("지금까지 장착한 장비 누적 수(gear_bonus)")] public int gearTotal; public int star = 1; public int shards; public int[] skillLv = { 1, 1 }; } [Serializable] public class UnitState : LevelState { public int count = 1; } [Serializable] public class Preset { [Tooltip("자리 0~2 = 전열 · 3~4 = 후열(playerSpawnPoints 순서) · 빈 칸 = \"\"")] public List slots = new List(); } [Serializable] public class StageState { [Tooltip("-")] public string key; public int stars; public int clears; } [Serializable] public class CountEntry { public string id; public int n; } [Serializable] public class Books { public int exp; public int skill; } [Serializable] public class Inventory { public List gear = new List(); public Books books = new Books(); public int blueprint; public int iron; // 보물(PD #1093 C · §11 inv.chests · inv.treasures) — 상자 보유 수 · 상자 포인트(명장 상자 게이지) · 수집품 public List chests = new List(); public int chestPoints; public List treasures = new List(); public TreasureState Treasure(string id) { foreach (var t in treasures) if (t.id == id) return t; return null; } public int GearCount(string id) { foreach (var e in gear) if (e.id == id) return e.n; return 0; } public void AddGear(string id, int n) { foreach (var e in gear) { if (e.id != id) continue; e.n = Mathf.Max(0, e.n + n); if (e.n == 0) gear.Remove(e); return; } if (n > 0) gear.Add(new CountEntry { id = id, n = n }); } } [Serializable] public class AdsUsed { [Tooltip("yyyyMMdd(기기 현지 날짜) · 바뀌면 일일 횟수 초기화")] public string day = ""; public int stamina; [Tooltip("순찰 광고 2배 수확(일)")] public int patrol; public int gacha; [Tooltip("보옥 → 군량 구매 횟수(일)")] public int gemStamina; } [Serializable] public class GachaState { [Tooltip("마지막 ★★★ 이후 뽑은 횟수(천장 카운터)")] public int pity; public int total; public bool firstTenUsed; } [Serializable] public class ShopItem { public string gearId; public int price; public bool sold; } [Serializable] public class ShopState { public string day = ""; public List items = new List(); public int refreshes; [Tooltip("스킬서 임시 판매(PD #1093 B) — 구매한 날(yyyyMMdd) · 그날 구매 수")] public string bookDay = ""; public int bookBought; } [Serializable] public class SaveData { public int ver = SaveStore.Version; public int gold; public int gem; public int stamina; [Tooltip("군량 회복 기준 시각(UTC · ISO 8601) — 오프라인 회복은 이 시각부터 계산")] public string staminaAt = ""; public AdsUsed adsUsed = new AdsUsed(); public List heroes = new List(); public List units = new List(); public List presets = new List(); public int presetIdx; public List stages = new List(); [Tooltip("클리어한 최고 챕터(레벨 상한 · 최소 1)")] public int chapterMax = 1; public Inventory inv = new Inventory(); public GachaState gacha = new GachaState(); public ShopState shop = new ShopState(); // 내정·보물·연무장(PD #1093 C · §11) public List training = new List(); public PatrolState patrol = new PatrolState(); public DevState dev = new DevState(); [Tooltip("보물 조건 효과 카운터(cond_key → 누적)")] public List counters = new List(); public ArenaState arena = new ArenaState(); public DailyC dailyC = new DailyC(); public string savedAt = ""; public TrainState Train(UnitType t) { var key = UnitTypeInfo.Key(t); foreach (var s in training) if (s.type == key) return s; var n = new TrainState { type = key }; training.Add(n); return n; } public HeroState Hero(string id) { foreach (var h in heroes) if (h.id == id) return h; return null; } public UnitState Unit(string id) { foreach (var u in units) if (u.id == id) return u; return null; } /// 영웅 또는 병사 유닛의 레벨 상태(미보유 = null). public LevelState Level(string id) { var h = Hero(id); if (h != null) return h; return Unit(id); } public bool Owns(string id) { return Level(id) != null; } public StageState Stage(int chapter, int stage, bool create) { string key = chapter.ToString(CultureInfo.InvariantCulture) + "-" + stage.ToString(CultureInfo.InvariantCulture); foreach (var s in stages) if (s.key == key) return s; if (!create) return null; var n = new StageState { key = key }; stages.Add(n); return n; } public Preset ActivePreset { get { return presets.Count == 0 ? null : presets[Mathf.Clamp(presetIdx, 0, presets.Count - 1)]; } } } /// 세이브 읽기·쓰기. 테스트·검증 캡처는 메모리 전용(MemoryOnly)으로 실제 save.json 을 건드리지 않는다. public static class SaveStore { public const int Version = 1; public const string FileName = "save.json"; /// 에디터 SessionState 키 — 검증 캡처(DataImporter.CaptureOutGame)가 Play 진입 전에 켠다(도메인 리로드 뒤에도 유지). public const string MemoryFlagKey = "TKS_SAVE_MEMORY"; static bool memoryOnly; static string memory; /// Play 진입마다 메모리 전용 해제(도메인 리로드를 끈 설정에서도 테스트의 메모리 모드가 PD 플레이로 새지 않게). [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void ResetStatics() { memoryOnly = false; memory = null; } public static string FilePath { get { return Path.Combine(Application.persistentDataPath, FileName); } } public static bool MemoryOnly { get { #if UNITY_EDITOR if (UnityEditor.SessionState.GetBool(MemoryFlagKey, false)) return true; #endif return memoryOnly; } } /// 메모리 전용으로 전환(fresh = 비운 상태에서 시작 = 신규 유저). public static void UseMemory(bool fresh) { memoryOnly = true; if (fresh) memory = null; } public static string ReadText() { if (MemoryOnly) return memory; try { return File.Exists(FilePath) ? File.ReadAllText(FilePath) : null; } catch (Exception e) { Debug.LogError("[TKS] 세이브 읽기 실패 " + FilePath + ": " + e.Message); return null; } } /// null = 없음(신규 유저) 또는 해석 실패. public static SaveData Load() { var text = ReadText(); if (string.IsNullOrEmpty(text)) return null; try { var d = JsonUtility.FromJson(text); if (d == null) return null; if (d.ver > Version) Debug.LogWarning("[TKS] 세이브 버전 " + d.ver + " > " + Version + " — 아는 필드만 읽음"); return d; } catch (Exception e) { Debug.LogError("[TKS] 세이브 해석 실패: " + e.Message); return null; } } public static void Write(SaveData d, DateTime nowUtc) { d.ver = Version; d.savedAt = Iso(nowUtc); var json = JsonUtility.ToJson(d, true); if (MemoryOnly) { memory = json; return; } try { var tmp = FilePath + ".tmp"; File.WriteAllText(tmp, json); if (File.Exists(FilePath)) File.Delete(FilePath); File.Move(tmp, FilePath); } catch (Exception e) { Debug.LogError("[TKS] 세이브 쓰기 실패 " + FilePath + ": " + e.Message); } } public static string Iso(DateTime utc) { return utc.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture); } public static DateTime ParseIso(string s, DateTime fallback) { DateTime t; if (!string.IsNullOrEmpty(s) && DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out t)) return t.ToUniversalTime(); return fallback; } } }