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;
}
}
}