ThreeKingdomsSlingshot/Assets/Scripts/OutGame/SaveData.cs

344 lines
11 KiB
C#

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<string> gear = new List<string>();
[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<string> slots = new List<string>();
}
[Serializable]
public class StageState
{
[Tooltip("<chapter>-<stage>")]
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<CountEntry> gear = new List<CountEntry>();
public Books books = new Books();
public int blueprint;
public int iron;
// 보물(PD #1093 C · §11 inv.chests · inv.treasures) — 상자 보유 수 · 상자 포인트(명장 상자 게이지) · 수집품
public List<CountEntry> chests = new List<CountEntry>();
public int chestPoints;
public List<TreasureState> treasures = new List<TreasureState>();
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<ShopItem> items = new List<ShopItem>();
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<HeroState> heroes = new List<HeroState>();
public List<UnitState> units = new List<UnitState>();
public List<Preset> presets = new List<Preset>();
public int presetIdx;
public List<StageState> stages = new List<StageState>();
[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<TrainState> training = new List<TrainState>();
public PatrolState patrol = new PatrolState();
public DevState dev = new DevState();
[Tooltip("보물 조건 효과 카운터(cond_key → 누적)")]
public List<CountEntry> counters = new List<CountEntry>();
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;
}
/// <summary>영웅 또는 병사 유닛의 레벨 상태(미보유 = null).</summary>
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)]; }
}
}
/// <summary>세이브 읽기·쓰기. 테스트·검증 캡처는 메모리 전용(MemoryOnly)으로 실제 save.json 을 건드리지 않는다.</summary>
public static class SaveStore
{
public const int Version = 1;
public const string FileName = "save.json";
/// <summary>에디터 SessionState 키 — 검증 캡처(DataImporter.CaptureOutGame)가 Play 진입 전에 켠다(도메인 리로드 뒤에도 유지).</summary>
public const string MemoryFlagKey = "TKS_SAVE_MEMORY";
static bool memoryOnly;
static string memory;
/// <summary>Play 진입마다 메모리 전용 해제(도메인 리로드를 끈 설정에서도 테스트의 메모리 모드가 PD 플레이로 새지 않게).</summary>
[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;
}
}
/// <summary>메모리 전용으로 전환(fresh = 비운 상태에서 시작 = 신규 유저).</summary>
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;
}
}
/// <summary>null = 없음(신규 유저) 또는 해석 실패.</summary>
public static SaveData Load()
{
var text = ReadText();
if (string.IsNullOrEmpty(text)) return null;
try
{
var d = JsonUtility.FromJson<SaveData>(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;
}
}
}