using System; using System.Collections.Generic; using UnityEngine; namespace TKS.OutGame { /// 순찰 수확 1회(미리보기 · 실제 지급 공용). public class PatrolYield { public float hours; public int gold; public int exp; public string gearGrade; public int gear; public int chests; public float gearF; public float chestF; public float mult = 1f; public List gearIds = new List(); public List expTo = new List(); public List chestIds = new List(); } /// /// 내정 순찰 규칙(기획서 §7 · S10 · PD #1093 C) — 마지막 클리어 챕터(chapterMax)의 patrol.csv 행 × 누적 시간(오프라인 포함 · 상한 patrol_cap_hours + 농업 patrol_cap_min) · /// 골드(× 순찰 수확·순찰 골드 개발) · 병사 경험(출전 편성 전원 · × 순찰 수확·경험 개발) · 승급 장비(patrol_gear_per_hour × 장비 확률 개발 · 등급 = gear_grade + 둔전제) · /// 청동 상자(chest_bronze_per_hour) — 장비·상자 소수점은 다음 수확으로 이월(결정적) · 광고 2배(patrol_ad_daily/일 · 스텁) · 즉시 수확(보옥 · patrol_instant_hours 분 · 타이머 그대로). /// public static class PatrolRules { public static float CapHours(OutGameData db, SaveData d) { return db.Economy.PatrolCapHours + DevRules.Mod(db, d, DevRules.PatrolCap) / 60f; } public static DateTime StartAt(SaveData d, DateTime nowUtc) { var t = SaveStore.ParseIso(d.patrol.startAt, nowUtc); return t > nowUtc ? nowUtc : t; } /// 지금까지 쌓인 시간(상한에서 멈춤). public static float Hours(OutGameData db, SaveData d, DateTime nowUtc) { float h = (float)(nowUtc - StartAt(d, nowUtc)).TotalHours; return Mathf.Clamp(h, 0f, CapHours(db, d)); } public static PatrolTable.Row Row(OutGameData db, SaveData d) { return db.Patrol.For(Mathf.Max(1, d.chapterMax)); } /// 순찰 장비 등급 = 표 gear_grade + 둔전제 Lv(최고 등급에서 멈춤). public static string GearGrade(OutGameData db, SaveData d) { var row = Row(db, d); if (row == null) return ""; int order = db.Gear.OrderOf(row.gearGrade) + Mathf.RoundToInt(DevRules.Mod(db, d, DevRules.PatrolGearGrade)); string best = row.gearGrade; int bestOrder = 0; foreach (var it in db.Gear.items) if (it.order <= order && it.order > bestOrder) { bestOrder = it.order; best = it.grade; } return best; } public static float GoldPerHour(OutGameData db, SaveData d) { var row = Row(db, d); return row == null ? 0f : row.goldPerHour * (1f + (DevRules.Mod(db, d, DevRules.PatrolYield) + DevRules.Mod(db, d, DevRules.PatrolGold)) / 100f); } public static float ExpPerHour(OutGameData db, SaveData d) { var row = Row(db, d); return row == null ? 0f : row.expPerHour * (1f + (DevRules.Mod(db, d, DevRules.PatrolYield) + DevRules.Mod(db, d, DevRules.ExpGain)) / 100f); } public static float GearPerHour(OutGameData db, SaveData d) { return db.Economy.PatrolGearPerHour * DevRules.Mult(db, d, DevRules.PatrolGearChance) * DevRules.Mult(db, d, DevRules.PatrolYield); } public static float ChestPerHour(OutGameData db, SaveData d) { var row = Row(db, d); return row == null ? 0f : row.chestPerHour * DevRules.Mult(db, d, DevRules.PatrolYield); } /// hours 시간분 수확량(mult = 광고 2배 등 · 이월 소수점 포함). public static PatrolYield Preview(OutGameData db, SaveData d, float hours, float mult) { var y = new PatrolYield { hours = hours, mult = mult, gearGrade = GearGrade(db, d) }; y.gold = Mathf.RoundToInt(GoldPerHour(db, d) * hours * mult); y.exp = Mathf.RoundToInt(ExpPerHour(db, d) * hours * mult); y.gearF = GearPerHour(db, d) * hours * mult + d.patrol.carryGear; y.chestF = ChestPerHour(db, d) * hours * mult + d.patrol.carryChest; y.gear = Mathf.FloorToInt(y.gearF + 1e-4f); y.chests = Mathf.FloorToInt(y.chestF + 1e-4f); return y; } static void Pay(OutGameData db, SaveData d, PatrolYield y, IList expTo, System.Random rng) { d.gold += y.gold; if (expTo != null) foreach (var id in expTo) { var s = d.Level(id); if (s == null || y.expTo.Contains(id)) continue; HeroRules.GrantExp(s, y.exp); y.expTo.Add(id); } var pool = new List(); foreach (var it in db.Gear.items) if (it.grade == y.gearGrade) pool.Add(it.id); for (int i = 0; i < y.gear && pool.Count > 0; i++) { var g = pool[rng.Next(pool.Count)]; d.inv.AddGear(g, 1); y.gearIds.Add(g); } for (int i = 0; i < y.chests; i++) { var got = TreasureRules.AddChest(db, d, db.Economy.PatrolChest, rng); if (got != null) y.chestIds.Add(got); } d.patrol.carryGear = Mathf.Max(0f, y.gearF - y.gear); d.patrol.carryChest = Mathf.Max(0f, y.chestF - y.chests); } /// 수확(쌓인 시간 전부 · 타이머 리셋 · 순찰 카운터 +1). ad = 광고 2배(하루 patrol_ad_daily 회 · 스텁). public static PatrolYield Harvest(OutGameData db, SaveData d, DateTime nowUtc, IList expTo, bool ad, System.Random rng, out string why) { why = null; float h = Hours(db, d, nowUtc); var e = db.Economy; EconomyRules.RollDaily(d, nowUtc); if (ad && d.adsUsed.patrol >= e.PatrolAdDaily) { why = "오늘 광고 2배 " + e.PatrolAdDaily + "회를 모두 썼습니다"; return null; } if (h <= 0f) { why = "쌓인 수확 없음"; return null; } var y = Preview(db, d, h, ad ? e.PatrolAdMult : 1f); Pay(db, d, y, expTo, rng); if (ad) d.adsUsed.patrol++; d.patrol.startAt = SaveStore.Iso(nowUtc); TreasureRules.AddCount(d, BattleEvents.Patrols, 1); return y; } /// 즉시 수확 = 보옥 patrol_instant_gem → patrol_instant_hours 시간분(쌓인 타이머는 그대로). public static PatrolYield Instant(OutGameData db, SaveData d, IList expTo, System.Random rng, out string why) { why = null; var e = db.Economy; if (d.gem < e.PatrolInstantGem) { why = "보옥 부족 " + d.gem + " / " + e.PatrolInstantGem; return null; } d.gem -= e.PatrolInstantGem; var y = Preview(db, d, e.PatrolInstantHours, 1f); Pay(db, d, y, expTo, rng); return y; } } }