2026-11-05 12:11:10 +00:00
|
|
|
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;
|
2026-11-05 15:22:54 +00:00
|
|
|
// 보물(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;
|
|
|
|
|
}
|
2026-11-05 12:11:10 +00:00
|
|
|
|
|
|
|
|
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;
|
2026-11-05 15:22:54 +00:00
|
|
|
[Tooltip("순찰 광고 2배 수확(일)")]
|
2026-11-05 12:11:10 +00:00
|
|
|
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;
|
2026-11-05 14:31:27 +00:00
|
|
|
[Tooltip("스킬서 임시 판매(PD #1093 B) — 구매한 날(yyyyMMdd) · 그날 구매 수")]
|
|
|
|
|
public string bookDay = "";
|
|
|
|
|
public int bookBought;
|
2026-11-05 12:11:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[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();
|
2026-11-05 15:22:54 +00:00
|
|
|
// 내정·보물·연무장(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();
|
2026-11-05 12:11:10 +00:00
|
|
|
public string savedAt = "";
|
|
|
|
|
|
2026-11-05 15:22:54 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-05 12:11:10 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|