334 lines
16 KiB
C#
334 lines
16 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace TKS.OutGame
|
||
{
|
||
/// <summary>
|
||
/// 영웅·병사 성장 규칙(기획서 §3) — 스탯 공식 · 전투력 · 레벨(골드 · 경험 · 챕터 상한) · 장착(1칸 = 1강) · 승급(6칸) · 진화(조각) · 신규 유저 세이브.
|
||
/// 수치는 전부 표(OutGameData)에서 읽는다(C45).
|
||
/// </summary>
|
||
public static class HeroRules
|
||
{
|
||
/// <summary>§3-2 스탯 내역(상세정보 표시 · 전투 유닛 생성 공용).</summary>
|
||
public struct Stats
|
||
{
|
||
public UnitType type;
|
||
public float baseHp, baseAtk, baseDef;
|
||
public float hpMult, atkMult, defMult;
|
||
public float lvPct, promoPct, gearPct, starPct;
|
||
/// <summary>훈련 %(공격 · HP·방어 = trainHpPct·trainDefPct · 내정 C).</summary>
|
||
public float trainPct, trainHpPct, trainDefPct;
|
||
/// <summary>보물 %(수집품 + 세트 · C).</summary>
|
||
public float treasureHpPct, treasureAtkPct, treasureDefPct;
|
||
public float hp, atk, def;
|
||
public int power;
|
||
|
||
/// <summary>항목 공통 성장 = (1+lv)(1+promo+gear)(1+star).</summary>
|
||
public float Core
|
||
{
|
||
get { return (1f + lvPct / 100f) * (1f + (promoPct + gearPct) / 100f) * (1f + starPct / 100f); }
|
||
}
|
||
|
||
/// <summary>영웅 배율을 뺀 항목별 성장 계수 = Core × (1+train(항목)) × (1+보물(항목)) · stat = hp|atk|def.</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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).
|
||
/// </summary>
|
||
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<string>();
|
||
for (int i = 0; i < slotCount; i++) s.gear.Add("");
|
||
return s;
|
||
}
|
||
|
||
/// <summary>요구 장비 칸 수(hero_gear.csv slot 최댓값).</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>신규 유저 = start_heroes 보유(Lv1 · 초기 등급 · ★ = 희귀도) · 재화 start_* · 프리셋 preset_count 개(모두 start_heroes 편성).</summary>
|
||
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<string>();
|
||
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;
|
||
}
|
||
|
||
/// <summary>미보유 → 보유 추가(true) · 이미 보유 = false.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>읽은 세이브의 빈 칸·크기를 표에 맞춘다(필드가 늘어난 구버전 · 손상 대비).</summary>
|
||
public static void Sanitize(OutGameData db, SaveData d, int formationSlots)
|
||
{
|
||
int gearSlots = GearSlotCount(db);
|
||
if (d.presets == null) d.presets = new List<Preset>();
|
||
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<string>();
|
||
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<string>();
|
||
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<StageState>();
|
||
// C 단계(PD #1093 C) — 구버전 세이브 = 비어 있음
|
||
if (d.training == null) d.training = new List<TrainState>();
|
||
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<CountEntry>();
|
||
if (d.counters == null) d.counters = new List<CountEntry>();
|
||
if (d.inv.chests == null) d.inv.chests = new List<CountEntry>();
|
||
if (d.inv.treasures == null) d.inv.treasures = new List<TreasureState>();
|
||
if (d.arena == null) d.arena = new ArenaState();
|
||
if (d.arena.samples == null) d.arena.samples = new List<ArenaSample>();
|
||
if (d.dailyC == null) d.dailyC = new DailyC();
|
||
if (d.dailyC.dungeons == null) d.dailyC.dungeons = new List<CountEntry>();
|
||
}
|
||
|
||
// ---------- 레벨 ----------
|
||
|
||
public static int LevelCap(OutGameData db, SaveData d)
|
||
{
|
||
return Mathf.Min(db.Levels.MaxLevel, db.Levels.CapFor(d.chapterMax));
|
||
}
|
||
|
||
/// <summary>다음 상한을 여는 챕터(없으면 0).</summary>
|
||
public static int NextCapChapter(OutGameData db, int lv)
|
||
{
|
||
var r = db.Levels.Get(lv + 1);
|
||
return r != null ? r.capChapter : 0;
|
||
}
|
||
|
||
/// <summary>null = 레벨업 가능 · 아니면 사유.</summary>
|
||
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;
|
||
}
|
||
|
||
// ---------- 승급 장비 · 승급 ----------
|
||
|
||
/// <summary>현재 등급 칸 slot 에 필요한 장비 id(최고 등급·병사 = null).</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>가방의 요구 장비 1개를 빈 칸에 장착(1칸 = 1강 · gearTotal +1).</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>6칸 모두 찬 상태 → 장비 소모(칸 비움) · 등급 +1(promo_pct · 스킬 상한).</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>스킬 레벨 상한 = 승급 등급 순서(녹 1 · 청 2 …) · ★3 액티브 +1 은 B 단계(skill_unlock) 처리.</summary>
|
||
public static int SkillCap(OutGameData db, HeroState s)
|
||
{
|
||
var p = s != null ? db.Promotion.Get(s.grade) : null;
|
||
return p != null ? p.order : 1;
|
||
}
|
||
|
||
// ---------- 진화 ----------
|
||
|
||
/// <summary>다음 ★ 에 필요한 조각(최고 ★ = 0).</summary>
|
||
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;
|
||
}
|
||
}
|
||
}
|