ThreeKingdomsSlingshot/Assets/Scripts/OutGame/Treasure/TreasureRules.cs

345 lines
14 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.Collections.Generic;
using UnityEngine;
namespace TKS.OutGame
{
/// <summary>상자 1개 열기 결과(화면 표시).</summary>
public class ChestResult
{
public string chestId;
public string treasureId;
public string grade;
public bool isNew;
public int shards;
public bool gradeUp;
public int gold;
public int gem;
public int iron;
public int points;
/// <summary>상자 포인트가 목표를 넘겨 받은 보상 상자 수.</summary>
public int bonusChests;
}
/// <summary>
/// 보물 수집품 규칙(기획서 §8 · S8 · PD #1093 C · 카피바라 수집품 구조) — 보유만으로 상시(슬롯 없음).
/// 효과 % = base_pct × (1 + treasure_star_pct × ★) + min(카운터 / cond_every, cond_max) × cond_pct(조건 효과 = base_stat 에 가산 · T2 해석) ·
/// 세트 = 가장 높은 달성 단계 1개(set = 전원 보유 · Nstar = 전원 ★N 이상) · 강화 ★n = 조각 shard_per_star × n + 운철 iron_per_star × n × 등급 배율 ·
/// hp/atk/def → HeroRules 스탯 공식 · crit/range/splash → 전투 진영 효과(SkillMods 파이프라인) · 상자 = chests.csv 확률 · 중복 = 조각 shards_dup · 상자 포인트 → 명장 상자.
/// </summary>
public static class TreasureRules
{
public static readonly string[] SetEffectKeys = { "hp_pct", "atk_pct", "def_pct" };
// ---------- 카운터(조건 효과) ----------
public static int Counter(SaveData d, string key)
{
return Counts.Get(d.counters, key);
}
public static void AddCount(SaveData d, string key, int n)
{
if (n > 0) Counts.Add(d.counters, key, n);
}
// ---------- 효과 ----------
public static int CondStacks(SaveData d, TreasureDb.Item it)
{
if (it.condEvery <= 0) return 0;
return Mathf.Min(it.condMax, Counter(d, it.condKey) / it.condEvery);
}
public static float BasePct(OutGameData db, TreasureDb.Item it, int star)
{
return it.basePct * (1f + db.Economy.TreasureStarPct * star);
}
/// <summary>보유 보물 1개의 효과 %(미보유 = 0).</summary>
public static float Value(OutGameData db, SaveData d, TreasureDb.Item it)
{
var st = d.inv.Treasure(it.id);
if (st == null) return 0f;
return BasePct(db, it, st.star) + CondStacks(d, it) * it.condPct;
}
/// <summary>stat(hp|atk|def|crit|range|splash) 보너스 % 합 = 보물 + 세트.</summary>
public static float StatPct(OutGameData db, SaveData d, string stat)
{
if (db == null || db.Treasures == null || d == null || d.inv == null) return 0f;
float s = 0f;
foreach (var it in db.Treasures.items)
if (it.baseStat == stat) s += Value(db, d, it);
string key = stat + "_pct";
foreach (var id in db.TreasureSets.SetIds)
{
var tier = SetTier(db, d, id);
if (tier != null && tier.effectKey == key) s += tier.pct;
}
return s;
}
public static int OwnedIn(SaveData d, TreasureSetDb.Tier tier)
{
int n = 0;
foreach (var m in tier.members)
if (d.inv.Treasure(m) != null) n++;
return n;
}
public static bool TierReached(SaveData d, TreasureSetDb.Tier tier)
{
foreach (var m in tier.members)
{
var st = d.inv.Treasure(m);
if (st == null || st.star < tier.stars) return false;
}
return true;
}
/// <summary>세트의 달성한 가장 높은 단계(없으면 null).</summary>
public static TreasureSetDb.Tier SetTier(OutGameData db, SaveData d, string setId)
{
TreasureSetDb.Tier best = null;
foreach (var t in db.TreasureSets.TiersOf(setId))
if (TierReached(d, t)) best = t;
return best;
}
/// <summary>전투 진영 효과(보물 crit·range·splash → SkillMods · 아군만).</summary>
public static List<SkillEffect> BattleEffects(OutGameData db, SaveData d)
{
var l = new List<SkillEffect>();
float crit = StatPct(db, d, "crit");
float range = StatPct(db, d, "range");
float splash = StatPct(db, d, "splash");
if (crit > 0f) l.Add(new SkillEffect { Key = SkillMods.SideCritPct, P = crit / 100f });
if (range > 0f) l.Add(new SkillEffect { Key = SkillKeys.RangeMult, P = 1f + range / 100f });
if (splash > 0f) l.Add(new SkillEffect { Key = SkillKeys.SplashRadiusMult, P = 1f + splash / 100f });
return l;
}
// ---------- 획득 · 강화 ----------
/// <summary>보물 획득: 처음 = 보유(★0) · 중복 = 조각 shards_dup. 반환 = 새로 얻음.</summary>
public static bool Grant(SaveData d, TreasureDb.Item it, out int shards)
{
shards = 0;
var st = d.inv.Treasure(it.id);
if (st == null)
{
d.inv.treasures.Add(new TreasureState { id = it.id, star = 0, shards = 0 });
return true;
}
st.shards += it.shardsDup;
shards = it.shardsDup;
return false;
}
public static void EnhanceCost(OutGameData db, TreasureDb.Item it, int toStar, out int shards, out int iron)
{
var e = db.Economy;
shards = e.TreasureShardPerStar * toStar;
iron = Mathf.RoundToInt(e.TreasureIronPerStar * toStar * e.TreasureIronMult(it.grade));
}
public static string CanEnhance(OutGameData db, SaveData d, string id)
{
var it = db.Treasures.Get(id);
var st = d.inv.Treasure(id);
if (it == null || st == null) return "미보유";
if (st.star >= it.starMax) return "최고 ★";
int shards, iron;
EnhanceCost(db, it, st.star + 1, out shards, out iron);
if (st.shards < shards) return "조각 " + st.shards + " / " + shards;
if (d.inv.iron < iron) return "운철 " + d.inv.iron + " / " + iron;
return null;
}
public static string Enhance(OutGameData db, SaveData d, string id)
{
var why = CanEnhance(db, d, id);
if (why != null) return why;
var it = db.Treasures.Get(id);
var st = d.inv.Treasure(id);
int shards, iron;
EnhanceCost(db, it, st.star + 1, out shards, out iron);
st.shards -= shards;
d.inv.iron -= iron;
st.star++;
return null;
}
// ---------- 상자 ----------
public static int ChestCount(SaveData d, string id)
{
return Counts.Get(d.inv.chests, id);
}
/// <summary>상자 받기(상자 등급 개발 chest_tier_up_pct = 한 단계 위 상자 · 포인트가 더 큰 다음 상자). 반환 = 실제로 받은 상자 id.</summary>
public static string AddChest(OutGameData db, SaveData d, string id, System.Random rng)
{
id = RollTier(db, d, id, rng);
if (id != null) Counts.Add(d.inv.chests, id, 1);
return id;
}
/// <summary>상자 등급 개발(chest_tier_up_pct) 확률로 한 단계 위 상자 id(표 순서에서 포인트가 더 큰 다음 상자) · 없는 상자 = null.</summary>
public static string RollTier(OutGameData db, SaveData d, string id, System.Random rng)
{
var c = db.Chests.Get(id);
if (c == null) return null;
float up = DevRules.Mod(db, d, DevRules.ChestTier) / 100f;
if (up > 0f && rng != null && rng.NextDouble() < up)
{
int i = db.Chests.IndexOf(id);
for (int k = i + 1; k < db.Chests.chests.Count; k++)
if (db.Chests.chests[k].points > c.points) return db.Chests.chests[k].id;
}
return id;
}
/// <summary>천기 등 상자 확률에 없는 등급 보물 1개(적장 토벌 전용).</summary>
public static string GrantGrade(OutGameData db, SaveData d, string grade, System.Random rng, out bool isNew, out int shards)
{
isNew = false;
shards = 0;
var pool = db.Treasures.OfGrade(grade);
if (pool.Count == 0) return null;
var it = pool[rng.Next(pool.Count)];
isNew = Grant(d, it, out shards);
return it.id;
}
/// <summary>상자 1개 열기: 등급 확률(상위 확률 개발 = 한 등급 위) → 그 등급 보물 1개 · 골드/보옥/운철 범위 · 상자 포인트(+개발 %) → 목표 넘으면 보상 상자.</summary>
public static ChestResult Open(OutGameData db, SaveData d, string chestId, System.Random rng)
{
var c = db.Chests.Get(chestId);
if (c == null || ChestCount(d, chestId) <= 0) return null;
Counts.Add(d.inv.chests, chestId, -1);
var r = new ChestResult { chestId = chestId };
double roll = rng.NextDouble(), acc = 0;
int gi = 0;
for (int i = 0; i < c.p.Length; i++)
{
acc += c.p[i];
if (c.p[i] > 0f) gi = i;
if (roll < acc && c.p[i] > 0f)
{
gi = i;
break;
}
}
float high = DevRules.Mod(db, d, DevRules.ChestHigh) / 100f;
if (high > 0f && gi + 1 < ChestDb.ProbGrades.Length && rng.NextDouble() < high)
{
gi++;
r.gradeUp = true;
}
for (int g = gi; g >= 0; g--)
{
var pool = db.Treasures.OfGrade(ChestDb.ProbGrades[g]);
if (pool.Count == 0) continue;
var it = pool[rng.Next(pool.Count)];
r.treasureId = it.id;
r.grade = it.grade;
r.isNew = Grant(d, it, out r.shards);
break;
}
r.gold = c.goldMax > 0 ? rng.Next(c.goldMin, c.goldMax + 1) : 0;
r.gem = c.gemMax > 0 ? rng.Next(c.gemMin, c.gemMax + 1) : 0;
r.iron = c.ironMax > 0 ? Mathf.RoundToInt(rng.Next(c.ironMin, c.ironMax + 1) * DevRules.Mult(db, d, DevRules.IronGain)) : 0;
d.gold += r.gold;
d.gem += r.gem;
d.inv.iron += r.iron;
r.points = Mathf.RoundToInt(c.points * DevRules.Mult(db, d, DevRules.ChestPoint));
d.inv.chestPoints += r.points;
int goal = Mathf.Max(1, db.Economy.ChestPointGoal);
while (d.inv.chestPoints >= goal)
{
d.inv.chestPoints -= goal;
Counts.Add(d.inv.chests, db.Economy.ChestPointReward, 1);
r.bonusChests++;
}
return r;
}
/// <summary>거래 상자 가격(currency = gold|gem|points · 0 = 판매 안 함) · 골드·보옥 = 거래 할인 개발 적용.</summary>
public static int Price(OutGameData db, SaveData d, ChestDb.Chest c, string currency)
{
int baseP = currency == "gold" ? c.buyGold : currency == "gem" ? c.buyGem : currency == "points" ? c.buyPoints : 0;
if (baseP <= 0 || currency == "points") return baseP;
return Mathf.Max(1, Mathf.RoundToInt(baseP * Mathf.Max(0f, 1f - DevRules.Mod(db, d, DevRules.TradeDiscount) / 100f)));
}
public static string Buy(OutGameData db, SaveData d, string chestId, string currency)
{
var c = db.Chests.Get(chestId);
if (c == null) return "없는 상자";
int price = Price(db, d, c, currency);
if (price <= 0) return "판매하지 않음";
if (currency == "gold")
{
if (d.gold < price) return "골드 부족 " + d.gold.ToString("N0") + " / " + price.ToString("N0");
d.gold -= price;
}
else if (currency == "gem")
{
if (d.gem < price) return "보옥 부족 " + d.gem.ToString("N0") + " / " + price.ToString("N0");
d.gem -= price;
}
else
{
if (d.inv.chestPoints < price) return "상자 포인트 부족 " + d.inv.chestPoints + " / " + price;
d.inv.chestPoints -= price;
}
Counts.Add(d.inv.chests, chestId, 1);
return null;
}
public static string GradeKo(string g)
{
switch (g)
{
case "rare": return "희귀";
case "epic": return "영웅";
case "legend": return "전설";
case "myth": return "신화";
case "arcana": return "천기";
}
return g;
}
public static string StatKo(string s)
{
switch (s)
{
case "hp": return "HP";
case "atk": return "공격력";
case "def": return "방어력";
case "crit": return "크리 피해";
case "range": return "사거리";
case "splash": return "광역 반경";
}
return s;
}
public static string CondKo(string k)
{
switch (k)
{
case BattleEvents.Battles: return "전투";
case BattleEvents.BossKills: return "보스 처치";
case BattleEvents.GoldSpent: return "골드 사용";
case BattleEvents.Patrols: return "순찰 수확";
case BattleEvents.StrategistShots: return "책사 사격";
case BattleEvents.ActiveUses: return "액티브 사용";
case BattleEvents.ArcherCrits: return "궁병 크리";
case BattleEvents.Pushes: return "밀기";
}
return k;
}
}
}