using System; using System.Collections.Generic; using UnityEngine; namespace TKS.OutGame { /// /// 아웃게임 실행 상태 = 표(OutGameData) + 세이브(SaveData). 바꾸는 동작은 전부 여기를 거쳐 즉시 저장(Commit)한다. /// 전투 연결 = 출전 편성(BuildLineup → BattleManager.LineupProvider) · 결과 보상(OnBattleEnd) · 군량(TryPayEntry) · 월드맵 진행(SyncWorldProgress). /// public class OutGameSession { public static OutGameSession Current { get; private set; } public readonly OutGameData Db; public readonly BalanceConfig Cfg; public SaveData Save; public System.Random Rng = new System.Random(Environment.TickCount); public Func Clock = () => DateTime.UtcNow; public StageReward LastReward; public bool IsNewUser { get; private set; } /// 마지막 연무장 결과(PD #1093 C). public ModeResult LastMode; int lastGold; public event Action Changed; public OutGameSession(OutGameData db, BalanceConfig cfg, SaveData save) { Db = db; Cfg = cfg; Save = save; } public DateTime Now { get { return Clock(); } } public int FormationSlots { get { return Cfg.playerSpawnPoints.Length; } } /// 세이브를 읽어(없으면 신규 유저) 오프라인 군량 회복 · 일일 초기화 · 월드맵 진행 반영. public static OutGameSession Start(OutGameData db, BalanceConfig cfg) { var now = DateTime.UtcNow; var save = SaveStore.Load(); bool isNew = save == null; if (isNew) save = HeroRules.NewSave(db, cfg.playerSpawnPoints.Length, now); var s = new OutGameSession(db, cfg, save) { IsNewUser = isNew }; HeroRules.Sanitize(db, save, s.FormationSlots); EconomyRules.TickStamina(db, save, now); EconomyRules.RollDaily(save, now); ArenaRules.RollDaily(db, save, now); s.lastGold = save.gold; s.SyncWorldProgress(); s.Commit(); Current = s; return s; } public void Commit() { // 보물 조건 효과 「골드 사용」 = 저장 사이에 줄어든 골드(쓰는 동작은 각자 Commit · PD #1093 C) if (Save.gold < lastGold) TreasureRules.AddCount(Save, BattleEvents.GoldSpent, lastGold - Save.gold); lastGold = Save.gold; SaveStore.Write(Save, Now); if (Changed != null) Changed(); } /// 군량 회복만 반영(화면 갱신 주기 · 저장은 값이 바뀔 때만). public void TickStamina() { int before = Save.stamina; EconomyRules.TickStamina(Db, Save, Now); if (Save.stamina != before) Commit(); } // ---------- 조회 ---------- public HeroDb.Hero Def(string id) { return Db.Heroes.Get(id); } public HeroRules.Stats StatsOf(string id) { return HeroRules.Compute(Db, Cfg, Def(id), Save.Level(id), Save); // 훈련 · 보물 포함(PD #1093 C) } /// 보유 목록(장수 = 영웅 · 병사 = 병사 유닛) · 표 순서. public List Owned(bool soldiers) { var res = new List(); foreach (var h in Db.Heroes.heroes) if (h.isSoldier == soldiers && Save.Owns(h.id)) res.Add(h); return res; } /// 지금 할 수 있는 성장(레벨업 · 장착 · 승급 · 진화)이 있는가 — 목록 카드 알림 점. public bool CanGrowAny(string id) { var h = Def(id); var lv = Save.Level(id); if (h == null || lv == null) return false; if (HeroRules.CanLevelUp(Db, Save, lv) == null) return true; var hs = Save.Hero(id); if (hs == null) return false; if (HeroRules.CanPromote(Db, h, hs) == null || HeroRules.CanEvolve(Db, h, hs) == null) return true; for (int i = 0; i < hs.gear.Count; i++) if (HeroRules.CanEquip(Db, Save, h, hs, i) == null) return true; return false; } public bool InActivePreset(string id) { var p = Save.ActivePreset; return p != null && p.slots.Contains(id); } // ---------- 성장 ---------- public string LevelUp(string id) { var s = Save.Level(id); var why = HeroRules.CanLevelUp(Db, Save, s); if (why != null) return why; HeroRules.LevelUp(Db, Save, s); Commit(); return null; } public string Equip(string id, int slot) { var h = Def(id); var s = Save.Hero(id); var why = HeroRules.CanEquip(Db, Save, h, s, slot); if (why != null) return why; HeroRules.Equip(Db, Save, h, s, slot); Commit(); return null; } public string Promote(string id) { var h = Def(id); var s = Save.Hero(id); var why = HeroRules.CanPromote(Db, h, s); if (why != null) return why; HeroRules.Promote(Db, h, s); Commit(); return null; } public string Evolve(string id) { var h = Def(id); var s = Save.Hero(id); var why = HeroRules.CanEvolve(Db, h, s); if (why != null) return why; HeroRules.Evolve(Db, h, s); Commit(); return null; } // ---------- 편성 ---------- /// 프리셋 idx 저장 + 출전 프리셋으로 지정(빈 칸 허용 · 최소 1). public string SavePreset(int idx, List slots) { if (idx < 0 || idx >= Save.presets.Count) return "없는 프리셋"; int filled = 0; foreach (var id in slots) { if (string.IsNullOrEmpty(id)) continue; if (!Save.Owns(id)) return "미보유 " + id; filled++; } if (filled == 0) return "최소 1명은 출전해야 합니다"; var p = Save.presets[idx]; p.slots.Clear(); for (int i = 0; i < FormationSlots; i++) p.slots.Add(i < slots.Count && slots[i] != null ? slots[i] : ""); Save.presetIdx = idx; Commit(); return null; } /// 출전 편성 = 출전 프리셋의 자리 순서(0~2 전열 · 3~4 후열 → playerSpawnPoints) · 스탯 = §3-2 공식. public List BuildLineup() { var res = new List(); var p = Save.ActivePreset; if (p == null) return res; for (int i = 0; i < p.slots.Count && i < FormationSlots; i++) { var id = p.slots[i]; var h = Def(id); if (h == null || !Save.Owns(id)) continue; var st = StatsOf(id); var entry = new LineupEntry { slot = i, id = h.id, name = h.nameKo, type = h.type, art = h.art, hpMult = h.hpMult * st.GrowthOf("hp"), atkMult = h.atkMult * st.GrowthOf("atk"), defMult = h.defMult * st.GrowthOf("def"), }; SkillRules.FillLineup(Db, Save, h, entry); // 스킬 id·레벨·★ 패시브 배율(PD #1093 B) res.Add(entry); } return res; } // ---------- 군량 ---------- public int StaminaSecondsToNext() { return EconomyRules.SecondsToNext(Db, Save, Now); } /// 스테이지 입장 비용 지불(부족 = false · 세이브 그대로). public bool TryPayEntry(BalanceConfig.StageDef s) { if (!EconomyRules.SpendStamina(Db, Save, EconomyRules.StageCost(Db, Save, s), Now)) return false; Commit(); return true; } public string BuyStamina() { var why = EconomyRules.BuyStaminaWithGem(Db, Save, Now); if (why == null) Commit(); return why; } public string AdStamina() { var why = EconomyRules.AdStamina(Db, Save, Now); if (why == null) Commit(); return why; } // ---------- 전투 결과 · 소탕 ---------- /// 전투 종료(승리·패배) → 보상 계산·반영·저장. 경험 = 출전 전원(Deployed). public StageReward OnBattleEnd(BattleManager bm, bool win) { var stage = bm.Stage; int alive = bm.AliveCount(Faction.Player); bool allAlive = bm.Deployed.Count > 0 && alive >= bm.Deployed.Count; var r = EconomyRules.Compute(Db, Save, stage, win, allAlive, bm.TurnNumber, false, Rng); r.stamina = EconomyRules.StageCost(Db, Save, stage); var ids = new List(); foreach (var e in bm.Deployed) ids.Add(e.id); EconomyRules.Apply(Save, r, ids); ArenaRules.CountBattle(Save, bm); // 보물 조건 효과(전투 · 보스 처치) LastReward = r; Commit(); return r; } /// 소탕(별 3 · 군량만 · 전투 없음) = 반복 보상 · 경험 = 출전 프리셋. null = 군량 부족·불가. public StageReward Sweep(BalanceConfig.StageDef s) { if (!EconomyRules.CanSweep(Save, s)) return null; int cost = EconomyRules.StageCost(Db, Save, s); if (!EconomyRules.SpendStamina(Db, Save, cost, Now)) return null; var r = EconomyRules.Compute(Db, Save, s, true, true, 0, true, Rng); r.stamina = cost; var ids = new List(); foreach (var e in BuildLineup()) ids.Add(e.id); EconomyRules.Apply(Save, r, ids); LastReward = r; Commit(); return r; } /// 월드맵 진행(챕터별 앞에서부터 연속으로 깬 노드 수) = 세이브 스테이지 기록. public void SyncWorldProgress() { WorldProgress.Reset(); for (int ch = 0; ch < Cfg.chapters.Length; ch++) { var list = Cfg.StagesOfChapter(Cfg.chapters[ch].no); int n = 0; while (n < list.Count) { var st = Cfg.stages[list[n]]; var ss = Save.Stage(st.chapter, st.stage, false); if (ss == null || ss.clears == 0) break; n++; } if (n > 0) WorldProgress.MarkCleared(ch, n - 1); } } // ---------- 거래 ---------- public List Draw(int count) { var res = GachaRules.Draw(Db, Save, count, Rng); if (res.Count > 0) Commit(); return res; } public void EnsureShop() { var day = Save.shop.day; int n = Save.shop.items.Count; EconomyRules.EnsureShop(Db, Save, Now, Rng); if (Save.shop.day != day || Save.shop.items.Count != n) Commit(); } public string BuyShop(int index) { var why = EconomyRules.BuyShop(Save, index); if (why == null) Commit(); return why; } public string RefreshShop() { var why = EconomyRules.RefreshShop(Db, Save, Rng); if (why == null) Commit(); return why; } // ---------- 내정 · 보물 · 연무장(PD #1093 C) ---------- /// 규칙 호출 → 성공(null)이면 저장 · 반환 = 실패 사유. public string Do(Func action) { var why = action(); if (why == null) Commit(); return why; } /// 출전 프리셋 id(순찰 경험 대상). public List PresetIds() { var ids = new List(); foreach (var e in BuildLineup()) ids.Add(e.id); return ids; } public PatrolYield Harvest(bool ad, out string why) { var y = PatrolRules.Harvest(Db, Save, Now, PresetIds(), ad, Rng, out why); if (y != null) Commit(); return y; } public PatrolYield InstantHarvest(out string why) { var y = PatrolRules.Instant(Db, Save, PresetIds(), Rng, out why); if (y != null) Commit(); return y; } public ChestResult OpenChest(string id) { var r = TreasureRules.Open(Db, Save, id, Rng); if (r != null) Commit(); return r; } /// 연무장 전투 문맥(결과 계산용) — kind · 대상(층 · 던전 id · 샘플 번호 · 보스 id). public class ModeCtx { public string kind; public string id; public int index; } public ModeCtx Mode; /// 연무장 전투 끝 → 보상·기록·저장(결과 패널 표시용). public ModeResult OnModeEnd(BattleManager bm, bool win) { var m = Mode; if (m == null) return null; ModeResult r = null; switch (m.kind) { case ArenaRules.Tower: r = ArenaRules.TowerResult(Db, Save, m.index, win, Rng); break; case ArenaRules.Dungeon: r = ArenaRules.DungeonResult(Db, Save, m.id, win, Now, Rng); break; case ArenaRules.Arena: r = ArenaRules.ArenaResult(Db, Save, m.index, win, Now); break; case ArenaRules.Raid: int dmg = 0; bool killed = false; foreach (var u in bm.Units) { if (u == null || u.Faction != Faction.Enemy || !u.IsBoss) continue; dmg += u.MaxHp - u.Hp; killed |= !u.IsAlive; } r = ArenaRules.RaidResult(Db, Save, m.id, dmg, killed, Now, Rng); break; } ArenaRules.CountBattle(Save, bm); LastMode = r; Mode = null; Commit(); return r; } } }