ThreeKingdomsSlingshot/Assets/Scripts/OutGame/OutGameSession.cs

318 lines
11 KiB
C#
Raw Normal View History

아웃게임 1차(PD #1093 세션 A) — 본영 로비 · 세이브 · 군량 · 영웅(레벨·승급 6칸·진화·스킬 표시) · 편성(전열 3/후열 2 · 프리셋 3) · 거래(뽑기·골드 장비 상점) · 스테이지 보상·별 3·소탕 - 데이터: 기획 tables/ 12장 → Assets/Data/OutGame/*.csv · 메뉴 TKS/Import OutGame Data(Editor/DataImporter) → Resources/OutGame 9 자산(HeroDb·GearDb·GachaConfig·EconomyConfig·RewardTable·LevelTable·PromotionTable·EvolveTable·SkillInfoDb) · 검증(행 수·필수 열·id 유일·hero↔skill↔gear·확률 합) 오류 시 자산 불변 · BalanceConfig.heroes[]/HeroDef 삭제 → HeroDb(기존 5인 id 유지) · economy.csv 키 17 + gacha.csv 키 2 추가(기획서 본문 값 · C45) - 세이브: persistentDataPath/save.json · ver 1 · 즉시 저장 · 오프라인 군량 회복 · 일일 초기화 · 신규 유저 = 유비·관우·장비·조운·황충 Lv1 녹 ★=희귀도 · 골드 1000 · 보옥 300 · 군량 30 · 테스트·캡처 = 메모리 전용 - 화면(uGUI 와이어프레임 · img03·06·08·11): 시작 = 본영 로비(출정 → 월드맵 · 편성 · 연무장/보물/내정 준비 중 · 동맹 추후) · 재화 바(월드맵 포함 · 군량 타이머) · 하단 탭 5 · ◀/Esc 일관 · 영웅 목록·상세·상세정보·도감 · 편성(탭·드래그) · 뽑기(천장 n/80 · 확률 공개 · 첫 10연 무료 · 10연 ★★ 보장 · 카드 순차 공개) · 장비 상점(흰/녹 6종 일일 · sell×5 · 새로고침 보옥 20) - 전투 연결: BattleManager.LineupProvider/Deployed(LineupEntry · §3-2 스탯 · 방어 배율 생성 뒤) · playerSpawnPoints = 전열 (−4/0/4, −12) · 후열 (−2/2, −14) · WorldMapScreen 훅(팝업 군량·보상·별 · 입장 군량 · 노드 별 star_on/off · 소탕 · ◀ 본영 · TopInset) · 결과 패널(별 3 조건 · 골드·경험·보옥·장비) · GameBootstrap/AutoPlayDriver 로비 시작 - B 세션 파일(ActionResolver·Combat·UnitView·BattleHUD·SlingshotInput·EnemyAI) 미변경 - 테스트 56/56(기존 41 유지 · 신규 OutGameTests 15) · console error 0 · 사본 GUI 캡처 11장(DataImporter.CaptureOutGame) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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;
res.Add(new LineupEntry
{
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,
});
}
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;
}
}
}