using System.Collections.Generic; using UnityEngine; namespace TKS.OutGame { /// 상자 1개 열기 결과(화면 표시). public class ChestResult { public string chestId; public string treasureId; public string grade; public bool isNew; public int shards; public bool gradeUp; public int gold; public int gem; public int iron; public int points; /// 상자 포인트가 목표를 넘겨 받은 보상 상자 수. public int bonusChests; } /// /// 보물 수집품 규칙(기획서 §8 · S8 · PD #1093 C · 카피바라 수집품 구조) — 보유만으로 상시(슬롯 없음). /// 효과 % = base_pct × (1 + treasure_star_pct × ★) + min(카운터 / cond_every, cond_max) × cond_pct(조건 효과 = base_stat 에 가산 · T2 해석) · /// 세트 = 가장 높은 달성 단계 1개(set = 전원 보유 · Nstar = 전원 ★N 이상) · 강화 ★n = 조각 shard_per_star × n + 운철 iron_per_star × n × 등급 배율 · /// hp/atk/def → HeroRules 스탯 공식 · crit/range/splash → 전투 진영 효과(SkillMods 파이프라인) · 상자 = chests.csv 확률 · 중복 = 조각 shards_dup · 상자 포인트 → 명장 상자. /// public static class TreasureRules { public static readonly string[] SetEffectKeys = { "hp_pct", "atk_pct", "def_pct" }; // ---------- 카운터(조건 효과) ---------- public static int Counter(SaveData d, string key) { return Counts.Get(d.counters, key); } public static void AddCount(SaveData d, string key, int n) { if (n > 0) Counts.Add(d.counters, key, n); } // ---------- 효과 ---------- public static int CondStacks(SaveData d, TreasureDb.Item it) { if (it.condEvery <= 0) return 0; return Mathf.Min(it.condMax, Counter(d, it.condKey) / it.condEvery); } public static float BasePct(OutGameData db, TreasureDb.Item it, int star) { return it.basePct * (1f + db.Economy.TreasureStarPct * star); } /// 보유 보물 1개의 효과 %(미보유 = 0). public static float Value(OutGameData db, SaveData d, TreasureDb.Item it) { var st = d.inv.Treasure(it.id); if (st == null) return 0f; return BasePct(db, it, st.star) + CondStacks(d, it) * it.condPct; } /// stat(hp|atk|def|crit|range|splash) 보너스 % 합 = 보물 + 세트. public static float StatPct(OutGameData db, SaveData d, string stat) { if (db == null || db.Treasures == null || d == null || d.inv == null) return 0f; float s = 0f; foreach (var it in db.Treasures.items) if (it.baseStat == stat) s += Value(db, d, it); string key = stat + "_pct"; foreach (var id in db.TreasureSets.SetIds) { var tier = SetTier(db, d, id); if (tier != null && tier.effectKey == key) s += tier.pct; } return s; } public static int OwnedIn(SaveData d, TreasureSetDb.Tier tier) { int n = 0; foreach (var m in tier.members) if (d.inv.Treasure(m) != null) n++; return n; } public static bool TierReached(SaveData d, TreasureSetDb.Tier tier) { foreach (var m in tier.members) { var st = d.inv.Treasure(m); if (st == null || st.star < tier.stars) return false; } return true; } /// 세트의 달성한 가장 높은 단계(없으면 null). public static TreasureSetDb.Tier SetTier(OutGameData db, SaveData d, string setId) { TreasureSetDb.Tier best = null; foreach (var t in db.TreasureSets.TiersOf(setId)) if (TierReached(d, t)) best = t; return best; } /// 전투 진영 효과(보물 crit·range·splash → SkillMods · 아군만). public static List BattleEffects(OutGameData db, SaveData d) { var l = new List(); float crit = StatPct(db, d, "crit"); float range = StatPct(db, d, "range"); float splash = StatPct(db, d, "splash"); if (crit > 0f) l.Add(new SkillEffect { Key = SkillMods.SideCritPct, P = crit / 100f }); if (range > 0f) l.Add(new SkillEffect { Key = SkillKeys.RangeMult, P = 1f + range / 100f }); if (splash > 0f) l.Add(new SkillEffect { Key = SkillKeys.SplashRadiusMult, P = 1f + splash / 100f }); return l; } // ---------- 획득 · 강화 ---------- /// 보물 획득: 처음 = 보유(★0) · 중복 = 조각 shards_dup. 반환 = 새로 얻음. public static bool Grant(SaveData d, TreasureDb.Item it, out int shards) { shards = 0; var st = d.inv.Treasure(it.id); if (st == null) { d.inv.treasures.Add(new TreasureState { id = it.id, star = 0, shards = 0 }); return true; } st.shards += it.shardsDup; shards = it.shardsDup; return false; } public static void EnhanceCost(OutGameData db, TreasureDb.Item it, int toStar, out int shards, out int iron) { var e = db.Economy; shards = e.TreasureShardPerStar * toStar; iron = Mathf.RoundToInt(e.TreasureIronPerStar * toStar * e.TreasureIronMult(it.grade)); } public static string CanEnhance(OutGameData db, SaveData d, string id) { var it = db.Treasures.Get(id); var st = d.inv.Treasure(id); if (it == null || st == null) return "미보유"; if (st.star >= it.starMax) return "최고 ★"; int shards, iron; EnhanceCost(db, it, st.star + 1, out shards, out iron); if (st.shards < shards) return "조각 " + st.shards + " / " + shards; if (d.inv.iron < iron) return "운철 " + d.inv.iron + " / " + iron; return null; } public static string Enhance(OutGameData db, SaveData d, string id) { var why = CanEnhance(db, d, id); if (why != null) return why; var it = db.Treasures.Get(id); var st = d.inv.Treasure(id); int shards, iron; EnhanceCost(db, it, st.star + 1, out shards, out iron); st.shards -= shards; d.inv.iron -= iron; st.star++; return null; } // ---------- 상자 ---------- public static int ChestCount(SaveData d, string id) { return Counts.Get(d.inv.chests, id); } /// 상자 받기(상자 등급 개발 chest_tier_up_pct = 한 단계 위 상자 · 포인트가 더 큰 다음 상자). 반환 = 실제로 받은 상자 id. public static string AddChest(OutGameData db, SaveData d, string id, System.Random rng) { id = RollTier(db, d, id, rng); if (id != null) Counts.Add(d.inv.chests, id, 1); return id; } /// 상자 등급 개발(chest_tier_up_pct) 확률로 한 단계 위 상자 id(표 순서에서 포인트가 더 큰 다음 상자) · 없는 상자 = null. public static string RollTier(OutGameData db, SaveData d, string id, System.Random rng) { var c = db.Chests.Get(id); if (c == null) return null; float up = DevRules.Mod(db, d, DevRules.ChestTier) / 100f; if (up > 0f && rng != null && rng.NextDouble() < up) { int i = db.Chests.IndexOf(id); for (int k = i + 1; k < db.Chests.chests.Count; k++) if (db.Chests.chests[k].points > c.points) return db.Chests.chests[k].id; } return id; } /// 천기 등 상자 확률에 없는 등급 보물 1개(적장 토벌 전용). public static string GrantGrade(OutGameData db, SaveData d, string grade, System.Random rng, out bool isNew, out int shards) { isNew = false; shards = 0; var pool = db.Treasures.OfGrade(grade); if (pool.Count == 0) return null; var it = pool[rng.Next(pool.Count)]; isNew = Grant(d, it, out shards); return it.id; } /// 상자 1개 열기: 등급 확률(상위 확률 개발 = 한 등급 위) → 그 등급 보물 1개 · 골드/보옥/운철 범위 · 상자 포인트(+개발 %) → 목표 넘으면 보상 상자. public static ChestResult Open(OutGameData db, SaveData d, string chestId, System.Random rng) { var c = db.Chests.Get(chestId); if (c == null || ChestCount(d, chestId) <= 0) return null; Counts.Add(d.inv.chests, chestId, -1); var r = new ChestResult { chestId = chestId }; double roll = rng.NextDouble(), acc = 0; int gi = 0; for (int i = 0; i < c.p.Length; i++) { acc += c.p[i]; if (c.p[i] > 0f) gi = i; if (roll < acc && c.p[i] > 0f) { gi = i; break; } } float high = DevRules.Mod(db, d, DevRules.ChestHigh) / 100f; if (high > 0f && gi + 1 < ChestDb.ProbGrades.Length && rng.NextDouble() < high) { gi++; r.gradeUp = true; } for (int g = gi; g >= 0; g--) { var pool = db.Treasures.OfGrade(ChestDb.ProbGrades[g]); if (pool.Count == 0) continue; var it = pool[rng.Next(pool.Count)]; r.treasureId = it.id; r.grade = it.grade; r.isNew = Grant(d, it, out r.shards); break; } r.gold = c.goldMax > 0 ? rng.Next(c.goldMin, c.goldMax + 1) : 0; r.gem = c.gemMax > 0 ? rng.Next(c.gemMin, c.gemMax + 1) : 0; r.iron = c.ironMax > 0 ? Mathf.RoundToInt(rng.Next(c.ironMin, c.ironMax + 1) * DevRules.Mult(db, d, DevRules.IronGain)) : 0; d.gold += r.gold; d.gem += r.gem; d.inv.iron += r.iron; r.points = Mathf.RoundToInt(c.points * DevRules.Mult(db, d, DevRules.ChestPoint)); d.inv.chestPoints += r.points; int goal = Mathf.Max(1, db.Economy.ChestPointGoal); while (d.inv.chestPoints >= goal) { d.inv.chestPoints -= goal; Counts.Add(d.inv.chests, db.Economy.ChestPointReward, 1); r.bonusChests++; } return r; } /// 거래 상자 가격(currency = gold|gem|points · 0 = 판매 안 함) · 골드·보옥 = 거래 할인 개발 적용. public static int Price(OutGameData db, SaveData d, ChestDb.Chest c, string currency) { int baseP = currency == "gold" ? c.buyGold : currency == "gem" ? c.buyGem : currency == "points" ? c.buyPoints : 0; if (baseP <= 0 || currency == "points") return baseP; return Mathf.Max(1, Mathf.RoundToInt(baseP * Mathf.Max(0f, 1f - DevRules.Mod(db, d, DevRules.TradeDiscount) / 100f))); } public static string Buy(OutGameData db, SaveData d, string chestId, string currency) { var c = db.Chests.Get(chestId); if (c == null) return "없는 상자"; int price = Price(db, d, c, currency); if (price <= 0) return "판매하지 않음"; if (currency == "gold") { if (d.gold < price) return "골드 부족 " + d.gold.ToString("N0") + " / " + price.ToString("N0"); d.gold -= price; } else if (currency == "gem") { if (d.gem < price) return "보옥 부족 " + d.gem.ToString("N0") + " / " + price.ToString("N0"); d.gem -= price; } else { if (d.inv.chestPoints < price) return "상자 포인트 부족 " + d.inv.chestPoints + " / " + price; d.inv.chestPoints -= price; } Counts.Add(d.inv.chests, chestId, 1); return null; } public static string GradeKo(string g) { switch (g) { case "rare": return "희귀"; case "epic": return "영웅"; case "legend": return "전설"; case "myth": return "신화"; case "arcana": return "천기"; } return g; } public static string StatKo(string s) { switch (s) { case "hp": return "HP"; case "atk": return "공격력"; case "def": return "방어력"; case "crit": return "크리 피해"; case "range": return "사거리"; case "splash": return "광역 반경"; } return s; } public static string CondKo(string k) { switch (k) { case BattleEvents.Battles: return "전투"; case BattleEvents.BossKills: return "보스 처치"; case BattleEvents.GoldSpent: return "골드 사용"; case BattleEvents.Patrols: return "순찰 수확"; case BattleEvents.StrategistShots: return "책사 사격"; case BattleEvents.ActiveUses: return "액티브 사용"; case BattleEvents.ArcherCrits: return "궁병 크리"; case BattleEvents.Pushes: return "밀기"; } return k; } } }