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