ThreeKingdomsSlingshot/Assets/Scripts/OutGame/HeroRules.cs

289 lines
13 KiB
C#
Raw 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, trainPct;
public float hp, atk, def;
public int power;
/// <summary>영웅 배율을 뺀 성장 계수 = (1+lv)(1+promo+gear)(1+star)(1+train).</summary>
public float Growth
{
get { return (1f + lvPct / 100f) * (1f + (promoPct + gearPct) / 100f) * (1f + starPct / 100f) * (1f + trainPct / 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)); } }
}
/// <summary>
/// stat = base(unit_type) × hero_mult × (1 + lvl_bonus) × (1 + promo_bonus + gear_bonus) × (1 + star_bonus) × (1 + train_bonus) · train = 0(A 단계) ·
/// 전투력 = 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)
{
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;
}
st.trainPct = 0f; // 내정 훈련 = C 단계(§7)
float g = st.Growth;
st.hp = b.maxHp * h.hpMult * g;
st.atk = b.atk * h.atkMult * g;
st.def = b.def * h.defMult * g;
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 };
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>();
}
// ---------- 레벨 ----------
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;
}
}
}