2026-11-05 12:11:10 +00:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
namespace TKS.OutGame
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 아웃게임 실행 상태 = 표(OutGameData) + 세이브(SaveData). 바꾸는 동작은 전부 여기를 거쳐 즉시 저장(Commit)한다.
|
|
|
|
|
/// 전투 연결 = 출전 편성(BuildLineup → BattleManager.LineupProvider) · 결과 보상(OnBattleEnd) · 군량(TryPayEntry) · 월드맵 진행(SyncWorldProgress).
|
|
|
|
|
/// </summary>
|
|
|
|
|
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<DateTime> Clock = () => DateTime.UtcNow;
|
|
|
|
|
public StageReward LastReward;
|
|
|
|
|
public bool IsNewUser { get; private set; }
|
|
|
|
|
|
|
|
|
|
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; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>세이브를 읽어(없으면 신규 유저) 오프라인 군량 회복 · 일일 초기화 · 월드맵 진행 반영.</summary>
|
|
|
|
|
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);
|
|
|
|
|
s.SyncWorldProgress();
|
|
|
|
|
s.Commit();
|
|
|
|
|
Current = s;
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Commit()
|
|
|
|
|
{
|
|
|
|
|
SaveStore.Write(Save, Now);
|
|
|
|
|
if (Changed != null) Changed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>군량 회복만 반영(화면 갱신 주기 · 저장은 값이 바뀔 때만).</summary>
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>보유 목록(장수 = 영웅 · 병사 = 병사 유닛) · 표 순서.</summary>
|
|
|
|
|
public List<HeroDb.Hero> Owned(bool soldiers)
|
|
|
|
|
{
|
|
|
|
|
var res = new List<HeroDb.Hero>();
|
|
|
|
|
foreach (var h in Db.Heroes.heroes)
|
|
|
|
|
if (h.isSoldier == soldiers && Save.Owns(h.id)) res.Add(h);
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>지금 할 수 있는 성장(레벨업 · 장착 · 승급 · 진화)이 있는가 — 목록 카드 알림 점.</summary>
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- 편성 ----------
|
|
|
|
|
|
|
|
|
|
/// <summary>프리셋 idx 저장 + 출전 프리셋으로 지정(빈 칸 허용 · 최소 1).</summary>
|
|
|
|
|
public string SavePreset(int idx, List<string> 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>출전 편성 = 출전 프리셋의 자리 순서(0~2 전열 · 3~4 후열 → playerSpawnPoints) · 스탯 = §3-2 공식.</summary>
|
|
|
|
|
public List<LineupEntry> BuildLineup()
|
|
|
|
|
{
|
|
|
|
|
var res = new List<LineupEntry>();
|
|
|
|
|
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);
|
|
|
|
|
float g = st.Growth;
|
2026-11-05 14:31:27 +00:00
|
|
|
var entry = new LineupEntry
|
2026-11-05 12:11:10 +00:00
|
|
|
{
|
|
|
|
|
slot = i, id = h.id, name = h.nameKo, type = h.type, art = h.art,
|
|
|
|
|
hpMult = h.hpMult * g, atkMult = h.atkMult * g, defMult = h.defMult * g,
|
2026-11-05 14:31:27 +00:00
|
|
|
};
|
|
|
|
|
SkillRules.FillLineup(Db, Save, h, entry); // 스킬 id·레벨·★ 패시브 배율(PD #1093 B)
|
|
|
|
|
res.Add(entry);
|
2026-11-05 12:11:10 +00:00
|
|
|
}
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- 군량 ----------
|
|
|
|
|
|
|
|
|
|
public int StaminaSecondsToNext()
|
|
|
|
|
{
|
|
|
|
|
return EconomyRules.SecondsToNext(Db, Save, Now);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>스테이지 입장 비용 지불(부족 = false · 세이브 그대로).</summary>
|
|
|
|
|
public bool TryPayEntry(BalanceConfig.StageDef s)
|
|
|
|
|
{
|
|
|
|
|
if (!EconomyRules.SpendStamina(Db, Save, EconomyRules.StageCost(Db, 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- 전투 결과 · 소탕 ----------
|
|
|
|
|
|
|
|
|
|
/// <summary>전투 종료(승리·패배) → 보상 계산·반영·저장. 경험 = 출전 전원(Deployed).</summary>
|
|
|
|
|
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, stage);
|
|
|
|
|
var ids = new List<string>();
|
|
|
|
|
foreach (var e in bm.Deployed) ids.Add(e.id);
|
|
|
|
|
EconomyRules.Apply(Save, r, ids);
|
|
|
|
|
LastReward = r;
|
|
|
|
|
Commit();
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>소탕(별 3 · 군량만 · 전투 없음) = 반복 보상 · 경험 = 출전 프리셋. null = 군량 부족·불가.</summary>
|
|
|
|
|
public StageReward Sweep(BalanceConfig.StageDef s)
|
|
|
|
|
{
|
|
|
|
|
if (!EconomyRules.CanSweep(Save, s)) return null;
|
|
|
|
|
int cost = EconomyRules.StageCost(Db, 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<string>();
|
|
|
|
|
foreach (var e in BuildLineup()) ids.Add(e.id);
|
|
|
|
|
EconomyRules.Apply(Save, r, ids);
|
|
|
|
|
LastReward = r;
|
|
|
|
|
Commit();
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>월드맵 진행(챕터별 앞에서부터 연속으로 깬 노드 수) = 세이브 스테이지 기록.</summary>
|
|
|
|
|
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<GachaPull> 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|