ThreeKingdomsSlingshot/Assets/Scripts/OutGame/HeroRules.cs

334 lines
16 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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