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;
}
}
}