using System; using System.Collections.Generic; using UnityEngine; namespace TKS.OutGame { /// /// 영웅·병사 성장 규칙(기획서 §3) — 스탯 공식 · 전투력 · 레벨(골드 · 경험 · 챕터 상한) · 장착(1칸 = 1강) · 승급(6칸) · 진화(조각) · 신규 유저 세이브. /// 수치는 전부 표(OutGameData)에서 읽는다(C45). /// public static class HeroRules { /// §3-2 스탯 내역(상세정보 표시 · 전투 유닛 생성 공용). public struct Stats { public UnitType type; public float baseHp, baseAtk, baseDef; public float hpMult, atkMult, defMult; public float lvPct, promoPct, gearPct, starPct; /// 훈련 %(공격 · HP·방어 = trainHpPct·trainDefPct · 내정 C). public float trainPct, trainHpPct, trainDefPct; /// 보물 %(수집품 + 세트 · C). public float treasureHpPct, treasureAtkPct, treasureDefPct; public float hp, atk, def; public int power; /// 항목 공통 성장 = (1+lv)(1+promo+gear)(1+star). public float Core { get { return (1f + lvPct / 100f) * (1f + (promoPct + gearPct) / 100f) * (1f + starPct / 100f); } } /// 영웅 배율을 뺀 항목별 성장 계수 = Core × (1+train(항목)) × (1+보물(항목)) · stat = hp|atk|def. public float GrowthOf(string stat) { float tr = stat == "hp" ? trainHpPct : stat == "def" ? trainDefPct : trainPct; float ts = stat == "hp" ? treasureHpPct : stat == "def" ? treasureDefPct : treasureAtkPct; return Core * (1f + tr / 100f) * (1f + ts / 100f); } public int Hp { get { return Mathf.Max(1, Mathf.RoundToInt(hp)); } } public int Atk { get { return Mathf.Max(1, Mathf.RoundToInt(atk)); } } public int Def { get { return Mathf.Max(0, Mathf.RoundToInt(def)); } } } public static Stats Compute(OutGameData db, BalanceConfig cfg, HeroDb.Hero h, LevelState s) { return Compute(db, cfg, h, s, null); } /// /// stat = base(unit_type) × hero_mult × (1 + lvl_bonus) × (1 + promo_bonus + gear_bonus) × (1 + star_bonus) × (1 + train_bonus(병종, 항목)) × (1 + 보물(항목)) · /// train = 내정 훈련(save 의 그 병종 · C) · 보물 = 수집품 + 세트(보유만으로 · 전 유닛 · C · 기획서에 위치 명시 없음 → 곱셈 1항 · PD 확인 대상) · save = null 이면 둘 다 0 · /// 전투력 = hp / power_hp_div + atk × power_atk_w + def × power_def_w. 병사 유닛 = 승급·장비·진화 없음(promo·gear·star = 0). /// public static Stats Compute(OutGameData db, BalanceConfig cfg, HeroDb.Hero h, LevelState s, SaveData save) { var b = cfg.GetStats(h.type); var hs = s as HeroState; var st = new Stats { type = h.type, baseHp = b.maxHp, baseAtk = b.atk, baseDef = b.def, hpMult = h.hpMult, atkMult = h.atkMult, defMult = h.defMult, }; var lvRow = db.Levels.Get(s != null ? s.lv : 1); st.lvPct = lvRow != null ? lvRow.statPct : 0f; if (!h.isSoldier && hs != null) { var p = db.Promotion.Get(hs.grade); st.promoPct = p != null ? p.promoPct : 0f; st.gearPct = hs.gearTotal * db.Economy.GearBonusPct; var ev = db.Evolve.Get(hs.star); st.starPct = ev != null ? ev.starPct : 0f; } if (save != null) { st.trainHpPct = TrainingRules.Bonus(db, save, h.type, "hp"); st.trainPct = TrainingRules.Bonus(db, save, h.type, "atk"); st.trainDefPct = TrainingRules.Bonus(db, save, h.type, "def"); st.treasureHpPct = TreasureRules.StatPct(db, save, "hp"); st.treasureAtkPct = TreasureRules.StatPct(db, save, "atk"); st.treasureDefPct = TreasureRules.StatPct(db, save, "def"); } st.hp = b.maxHp * h.hpMult * st.GrowthOf("hp"); st.atk = b.atk * h.atkMult * st.GrowthOf("atk"); st.def = b.def * h.defMult * st.GrowthOf("def"); var e = db.Economy; st.power = Mathf.RoundToInt(st.hp / Mathf.Max(0.0001f, e.PowerHpDiv) + st.atk * e.PowerAtkW + st.def * e.PowerDefW); return st; } // ---------- 신규 유저 ---------- public static HeroState NewHero(OutGameData db, HeroDb.Hero h, int slotCount) { var s = new HeroState { id = h.id, lv = 1, exp = 0, grade = db.Promotion.FirstGrade, star = Mathf.Clamp(h.rarity, 1, db.Evolve.MaxStar), shards = 0, gearTotal = 0 }; s.gear = new List(); for (int i = 0; i < slotCount; i++) s.gear.Add(""); return s; } /// 요구 장비 칸 수(hero_gear.csv slot 최댓값). public static int GearSlotCount(OutGameData db) { foreach (var h in db.Heroes.heroes) if (h.gear.Count > 0) return h.gear[0].slots.Count; return 0; } /// 신규 유저 = start_heroes 보유(Lv1 · 초기 등급 · ★ = 희귀도) · 재화 start_* · 프리셋 preset_count 개(모두 start_heroes 편성). public static SaveData NewSave(OutGameData db, int formationSlots, DateTime nowUtc) { var e = db.Economy; var d = new SaveData { gold = e.StartGold, gem = e.StartGem, stamina = e.StartStamina, staminaAt = SaveStore.Iso(nowUtc), chapterMax = 1 }; d.inv.books.skill = e.StartSkillBooks; // 신규 유저 스킬서(PD #1093 B) d.patrol.startAt = SaveStore.Iso(nowUtc); // 순찰 = 시작부터 누적(PD #1093 C) int gearSlots = GearSlotCount(db); var lineup = new List(); foreach (var id in e.StartHeroes) { var h = db.Heroes.Get(id); if (h == null) continue; Grant(db, d, h, gearSlots); lineup.Add(h.id); } for (int p = 0; p < Mathf.Max(1, e.PresetCount); p++) { var pr = new Preset(); for (int i = 0; i < formationSlots; i++) pr.slots.Add(i < lineup.Count ? lineup[i] : ""); d.presets.Add(pr); } return d; } /// 미보유 → 보유 추가(true) · 이미 보유 = false. public static bool Grant(OutGameData db, SaveData d, HeroDb.Hero h, int gearSlots) { if (d.Owns(h.id)) return false; if (h.isSoldier) d.units.Add(new UnitState { id = h.id, lv = 1, exp = 0, count = 1 }); else d.heroes.Add(NewHero(db, h, gearSlots)); return true; } /// 읽은 세이브의 빈 칸·크기를 표에 맞춘다(필드가 늘어난 구버전 · 손상 대비). public static void Sanitize(OutGameData db, SaveData d, int formationSlots) { int gearSlots = GearSlotCount(db); if (d.presets == null) d.presets = new List(); while (d.presets.Count < Mathf.Max(1, db.Economy.PresetCount)) d.presets.Add(new Preset()); foreach (var p in d.presets) { if (p.slots == null) p.slots = new List(); while (p.slots.Count < formationSlots) p.slots.Add(""); if (p.slots.Count > formationSlots) p.slots.RemoveRange(formationSlots, p.slots.Count - formationSlots); for (int i = 0; i < p.slots.Count; i++) if (p.slots[i] == null || (p.slots[i].Length > 0 && !d.Owns(p.slots[i]))) p.slots[i] = ""; } d.presetIdx = Mathf.Clamp(d.presetIdx, 0, d.presets.Count - 1); foreach (var h in d.heroes) { if (h.gear == null) h.gear = new List(); while (h.gear.Count < gearSlots) h.gear.Add(""); for (int i = 0; i < h.gear.Count; i++) if (h.gear[i] == null) h.gear[i] = ""; if (string.IsNullOrEmpty(h.grade) || db.Promotion.Get(h.grade) == null) h.grade = db.Promotion.FirstGrade; if (h.skillLv == null || h.skillLv.Length < 2) h.skillLv = new[] { 1, 1 }; h.star = Mathf.Clamp(h.star, 1, db.Evolve.MaxStar); h.lv = Mathf.Clamp(h.lv, 1, db.Levels.MaxLevel); } foreach (var u in d.units) u.lv = Mathf.Clamp(u.lv, 1, db.Levels.MaxLevel); if (d.chapterMax < 1) d.chapterMax = 1; if (d.inv == null) d.inv = new Inventory(); if (d.adsUsed == null) d.adsUsed = new AdsUsed(); if (d.gacha == null) d.gacha = new GachaState(); if (d.shop == null) d.shop = new ShopState(); if (d.stages == null) d.stages = new List(); // C 단계(PD #1093 C) — 구버전 세이브 = 비어 있음 if (d.training == null) d.training = new List(); foreach (var t in d.training) { if (t.lv == null || t.lv.Length != TrainingTable.Stats.Length) t.lv = new int[TrainingTable.Stats.Length]; t.rank = Mathf.Clamp(t.rank, 1, Mathf.Max(1, db.Training != null ? db.Training.MaxRank : 1)); } if (d.patrol == null) d.patrol = new PatrolState(); if (string.IsNullOrEmpty(d.patrol.startAt)) d.patrol.startAt = SaveStore.Iso(DateTime.UtcNow); if (d.dev == null) d.dev = new DevState(); if (d.dev.nodes == null) d.dev.nodes = new List(); if (d.counters == null) d.counters = new List(); if (d.inv.chests == null) d.inv.chests = new List(); if (d.inv.treasures == null) d.inv.treasures = new List(); if (d.arena == null) d.arena = new ArenaState(); if (d.arena.samples == null) d.arena.samples = new List(); if (d.dailyC == null) d.dailyC = new DailyC(); if (d.dailyC.dungeons == null) d.dailyC.dungeons = new List(); } // ---------- 레벨 ---------- public static int LevelCap(OutGameData db, SaveData d) { return Mathf.Min(db.Levels.MaxLevel, db.Levels.CapFor(d.chapterMax)); } /// 다음 상한을 여는 챕터(없으면 0). public static int NextCapChapter(OutGameData db, int lv) { var r = db.Levels.Get(lv + 1); return r != null ? r.capChapter : 0; } /// null = 레벨업 가능 · 아니면 사유. public static string CanLevelUp(OutGameData db, SaveData d, LevelState s) { if (s == null) return "미보유"; if (s.lv >= db.Levels.MaxLevel) return "최고 레벨"; if (s.lv >= LevelCap(db, d)) return "레벨 상한 Lv " + LevelCap(db, d) + " — " + NextCapChapter(db, s.lv) + "장을 클리어하면 해금"; var r = db.Levels.Get(s.lv); if (r == null) return "레벨 표에 Lv " + s.lv + " 없음"; if (s.exp < r.expToNext) return "경험 부족 " + s.exp + " / " + r.expToNext; if (d.gold < r.goldCost) return "골드 부족 " + d.gold + " / " + r.goldCost; return null; } public static bool LevelUp(OutGameData db, SaveData d, LevelState s) { if (CanLevelUp(db, d, s) != null) return false; var r = db.Levels.Get(s.lv); d.gold -= r.goldCost; s.exp -= r.expToNext; s.lv++; return true; } public static void GrantExp(LevelState s, int exp) { if (s != null && exp > 0) s.exp += exp; } // ---------- 승급 장비 · 승급 ---------- /// 현재 등급 칸 slot 에 필요한 장비 id(최고 등급·병사 = null). public static string RequiredGear(OutGameData db, HeroDb.Hero h, HeroState s, int slot) { var list = db.Heroes.GearFor(h, s != null ? s.grade : ""); return list != null && slot >= 0 && slot < list.Count ? list[slot] : null; } public static int EquippedCount(HeroState s) { int n = 0; if (s != null) foreach (var g in s.gear) if (!string.IsNullOrEmpty(g)) n++; return n; } public static string CanEquip(OutGameData db, SaveData d, HeroDb.Hero h, HeroState s, int slot) { if (h == null || s == null || h.isSoldier) return "승급 없음"; var need = RequiredGear(db, h, s, slot); if (need == null) return "최고 등급"; if (!string.IsNullOrEmpty(s.gear[slot])) return "이미 장착"; if (d.inv.GearCount(need) <= 0) return "장비 없음"; return null; } /// 가방의 요구 장비 1개를 빈 칸에 장착(1칸 = 1강 · gearTotal +1). public static bool Equip(OutGameData db, SaveData d, HeroDb.Hero h, HeroState s, int slot) { if (CanEquip(db, d, h, s, slot) != null) return false; var need = RequiredGear(db, h, s, slot); d.inv.AddGear(need, -1); s.gear[slot] = need; s.gearTotal++; return true; } public static string CanPromote(OutGameData db, HeroDb.Hero h, HeroState s) { if (h == null || s == null || h.isSoldier) return "승급 없음"; var p = db.Promotion.Get(s.grade); if (p == null || p.nextGrade.Length == 0) return "최고 등급"; var list = db.Heroes.GearFor(h, s.grade); if (list == null) return "요구 장비 표 없음"; int n = EquippedCount(s); if (n < list.Count) return "장비 " + n + " / " + list.Count + "칸"; return null; } /// 6칸 모두 찬 상태 → 장비 소모(칸 비움) · 등급 +1(promo_pct · 스킬 상한). public static bool Promote(OutGameData db, HeroDb.Hero h, HeroState s) { if (CanPromote(db, h, s) != null) return false; s.grade = db.Promotion.Get(s.grade).nextGrade; for (int i = 0; i < s.gear.Count; i++) s.gear[i] = ""; return true; } /// 스킬 레벨 상한 = 승급 등급 순서(녹 1 · 청 2 …) · ★3 액티브 +1 은 B 단계(skill_unlock) 처리. public static int SkillCap(OutGameData db, HeroState s) { var p = s != null ? db.Promotion.Get(s.grade) : null; return p != null ? p.order : 1; } // ---------- 진화 ---------- /// 다음 ★ 에 필요한 조각(최고 ★ = 0). public static int ShardsForNext(OutGameData db, HeroState s) { var r = s != null ? db.Evolve.Get(s.star + 1) : null; return r != null ? r.shardsNeeded : 0; } public static string CanEvolve(OutGameData db, HeroDb.Hero h, HeroState s) { if (h == null || s == null || h.isSoldier) return "진화 없음"; if (s.star >= db.Evolve.MaxStar) return "최고 ★"; int need = ShardsForNext(db, s); if (s.shards < need) return "조각 " + s.shards + " / " + need; return null; } public static bool Evolve(OutGameData db, HeroDb.Hero h, HeroState s) { if (CanEvolve(db, h, s) != null) return false; s.shards -= ShardsForNext(db, s); s.star++; return true; } } }