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

345 lines
14 KiB
C#
Raw Permalink Normal View History

아웃게임 2차(PD #1093 세션 C) — 내정(훈련·순찰·개발) · 보물(수집품·세트·상자) · 연무장(탑·일일 던전·투기장·적장 토벌) · 거래 상자 탭 + 영웅 56명 확장(PD #1096 · 신규 effect_key 4) - 데이터: C 표 10장(training · training_ranks · patrol · dev_tree · treasures · treasure_sets · chests(+iron/buy 열) · tower · dungeons(+reward_amount) · arena(+상점 키)) + economy.csv C 키 24 → 임포트 18 자산(OutGameTables.C.cs 검증: 행·연속 Lv/랭크/층·참조·확률 합 1·선행 노드·효과 키 처리기·카운터 키 · 오류 시 자산 불변) - 영웅 56명: heroes 71 · skills 112 · hero_gear 1344 · gacha_pool 71 재복사 · 신규 키 4(team_dmg_pct_turn · cooldown_add · charm · heal_self_pct) + 기존 키 조건 열(받는 피해 p2 HP 임계/보스/근접 · p3 출전 인원 아군 전원 · 최대 HP·반격 아군 전원 · 피해 % 발동 턴) · 대장군 키는 표 미사용(처리기 유지) - 내정: 훈련(병종 5 · 3항목 Lv · cost_mult · 랭크 승급 · train_bonus → §3-2 항목별) · 순찰(챕터 행 × 시간 · 오프라인 · 상한 12h + 농업 · 소수점 이월 · 광고 2배 스텁 · 즉시 수확 보옥) · 개발(갈래 3 · 선행 · 골드·도면 비용 · 효과 키 → 군량·보상·상점·순찰·상자 적용 지점 · 초기화 보옥 + 도면 환급) - 보물: 보유 중(4열 · 등급 색 · 상세·강화) · 세트(단계 효과) · 상자(포인트 → 명장 · 확률 · 중복 조각) · 효과 hp/atk/def → HeroRules · crit/range/splash → BattleManager.SideEffectsProvider → SkillMods · 조건 카운터 8종 = Runtime/BattleEvents(전투 코드 Raise 1줄씩 · 아군만) · 스테이지 청동 상자 10 % - 연무장: BattleManager.StartMode(BattleMode = 적 편성·제한 턴·onExit) · 탑 100층(입장권·층 배율·보상·10층 상자) · 던전 3(요일·2/일·보상) · 투기장(샘플 10 · EnemyAI 방어 · 메달·주간 순위·조각 상점) · 적장 토벌(주간 순환·hp ×20·5턴·누적 피해·명장 상자·천기) · 결과 패널 → 탭 = 연무장 - 거래: 상자 탭(구매 = 골드/보옥/포인트 · 거래 할인) · 패스·월정액·챕터 팩·첫 결제 팩 = 준비 중 스텁 · 스킬서 임시 판매 제거(사당 던전) - 테스트 87/87(기존 73 유지 · 갱신 3 = 로비 탭 · 표 키 목록 · 상점 스킬서 칸 / 신규 14 = OutGameCTests 12 + 확장 키 2) · console error 0 · 캡처 = DataImporter.CaptureC Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:22:54 +00:00
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;
}
}
}