ThreeKingdomsSlingshot/Assets/Scripts/Runtime/Skills/SkillRules.cs

138 lines
6.2 KiB
C#
Raw Permalink Normal View History

스킬 시스템(PD #1093 세션 B) — skills.csv effect_key 51종 처리기 · 액티브(예약/즉시)·패시브 · HUD 스킬 버튼 · 적 무장 패시브 · 스킬 레벨업·스킬서 - 데이터: SkillInfoDb(= 스킬 DB) 에 effect_key·p1~p3(pMask)·lv_scale·needs_impl 추가 · 임포터 검증(trigger · 키 51종 · p 칸 · skill_levels 연속 · lv_max) · skill_levels = levels(비용 표) - 처리기 Runtime/Skills: SkillKeys(51종 · 레벨 계수 = 값×계수 / 배율은 1 넘는 몫 / 횟수 반올림 / 켜기·치환 고정 · 팀 키) · SkillDef·SkillCatalog · SkillSystem(유닛별 쿨·예약 · 즉시형 시한 효과 이번 아군 턴/이번 적 턴 · 행동 훅 · 호위 분담 · HUD 한 줄) · SkillMods(전투 코드 수치 질의) · SkillRules(상한 = 등급 + ★3 액티브 +1 · ★5 패시브 ×1.5 = evolve.csv · 레벨업 · 스킬서 판매) - 전투 연결: ActionResolver(밀기·튕김·추가 이동 n·추가 이동 공격·충돌 추가 피해·밀며 전진 ×·삼첨도·관통 피해/넉백/반격 없음/빗나가도 복귀/한 번 더 관통·사거리·사선 통과·크리·광역 반경/비율/아군 0·백기습영 이동 후 사격·군령/대장군 공격 횟수) · Combat(상성 하한·공 %·주는/받는 피해·근접 패널티 해제·반격 배율/받는 반격·견제 배율) · BattleManager(편성·적 무장 부착 · 견제 반경/횟수/무제한/회복 · 교전 반응 정지 · 교전 범위 · 즉시형 실행) · EnemyAI·RangeIndicator(슬링·사거리 스킬 반영) - UI: HUD 스킬 버튼(이름·Lv·쿨·사용 가능/불가·패시브) · 탭 = 예약/취소/즉시 · 조준 중 2손가락 탭 · 발동 = 이름 글자 + 플래시 · 스킬 한 줄 · 예약 배지 · 변진 반경 원 · 영웅 상세 스킬 레벨업 팝업 · 거래 상점 스킬서 칸(임시) - 표: economy.csv start_skill_books 10 · skill_book_shop_daily 3 · skill_book_price 300(임시 · PD 확인) - 테스트 73/73(기존 56 + SkillTests 17 · 51종 전부) · console error 0 · 캡처 = DataImporter.CaptureSkills Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 14:31:27 +00:00
using System;
using System.Globalization;
using UnityEngine;
namespace TKS.OutGame
{
/// <summary>
/// 스킬 성장 규칙(시스템 기획 §3-5·§5 · PD #1093 B): 레벨 1~lv_max · 상한 = 승급 등급(HeroRules.SkillCap) + ★ 보정(evolve.csv skill_unlock 「active_lv+n」 = 액티브 상한 +n) ·
/// ★ 패시브 배율(「passive_x1.5」) · 레벨업 비용 = skill_levels.csv(스킬서 + 골드) · 스킬서 임시 판매(economy.csv skill_book_* · 획득처 사당 던전은 C 단계).
/// </summary>
public static class SkillRules
{
const string ActiveCapToken = "active_lv+";
const string PassiveMultToken = "passive_x";
/// <summary>★ star 까지 해금된 액티브 상한 보정 합(evolve.csv skill_unlock).</summary>
public static int ActiveCapBonus(OutGameData db, int star)
{
int add = 0;
if (db == null || db.Evolve == null) return 0;
foreach (var r in db.Evolve.rows)
{
if (r.star > star || string.IsNullOrEmpty(r.skillUnlock) || !r.skillUnlock.StartsWith(ActiveCapToken)) continue;
int n;
if (int.TryParse(r.skillUnlock.Substring(ActiveCapToken.Length), NumberStyles.Integer, CultureInfo.InvariantCulture, out n)) add += n;
}
return add;
}
/// <summary>★ star 까지 해금된 패시브 효과 배율(곱 · 없으면 1).</summary>
public static float PassiveMult(OutGameData db, int star)
{
float m = 1f;
if (db == null || db.Evolve == null) return m;
foreach (var r in db.Evolve.rows)
{
if (r.star > star || string.IsNullOrEmpty(r.skillUnlock) || !r.skillUnlock.StartsWith(PassiveMultToken)) continue;
float f;
if (float.TryParse(r.skillUnlock.Substring(PassiveMultToken.Length), NumberStyles.Float, CultureInfo.InvariantCulture, out f)) m *= f;
}
return m;
}
/// <summary>스킬 i(0 = 액티브 · 1 = 패시브) 레벨 상한 = 등급 순서 + (액티브면 ★ 보정) · 최대 lv_max.</summary>
public static int Cap(OutGameData db, HeroDb.Hero h, HeroState s, int i)
{
var sk = Skill(db, h, i);
if (sk == null || s == null) return 1;
int cap = HeroRules.SkillCap(db, s) + (i == 0 ? ActiveCapBonus(db, s.star) : 0);
return Mathf.Clamp(cap, 1, Mathf.Min(sk.lvMax, db.Skills.MaxLevel));
}
public static SkillInfoDb.Skill Skill(OutGameData db, HeroDb.Hero h, int i)
{
if (db == null || db.Skills == null || h == null) return null;
return db.Skills.Get(i == 0 ? h.skillActive : h.skillPassive);
}
public static int Level(HeroState s, int i)
{
return s != null && s.skillLv != null && s.skillLv.Length > i ? Mathf.Max(1, s.skillLv[i]) : 1;
}
/// <summary>레벨업 가능하면 null · 아니면 이유.</summary>
public static string CanLevelUp(OutGameData db, SaveData d, HeroDb.Hero h, HeroState s, int i)
{
if (h == null || s == null || h.isSoldier) return "병사 유닛은 스킬 없음";
var sk = Skill(db, h, i);
if (sk == null) return "스킬 없음";
int lv = Level(s, i);
int cap = Cap(db, h, s, i);
if (lv >= sk.lvMax) return "최고 레벨";
if (lv >= cap) return "상한 Lv " + cap + " — 승급" + (i == 0 ? "·★3" : "") + "으로 해금";
var c = db.Skills.CostTo(lv + 1);
if (c == null) return "비용 표 없음";
if (d.inv.books.skill < c.books) return "스킬서 " + d.inv.books.skill + " / " + c.books;
if (d.gold < c.gold) return "골드 " + d.gold.ToString("N0") + " / " + c.gold.ToString("N0");
return null;
}
public static string LevelUp(OutGameData db, SaveData d, HeroDb.Hero h, HeroState s, int i)
{
var why = CanLevelUp(db, d, h, s, i);
if (why != null) return why;
var c = db.Skills.CostTo(Level(s, i) + 1);
d.inv.books.skill -= c.books;
d.gold -= c.gold;
if (s.skillLv == null || s.skillLv.Length < 2) s.skillLv = new[] { 1, 1 };
s.skillLv[i] = Level(s, i) + 1;
return null;
}
/// <summary>출전 칸에 스킬(id · 레벨 = 상한 안 · ★ 패시브 배율 · 무장)을 채운다.</summary>
public static void FillLineup(OutGameData db, SaveData d, HeroDb.Hero h, LineupEntry e)
{
if (h == null || e == null || h.isSoldier) return;
var s = d.Hero(h.id);
e.general = true;
e.skillActive = h.skillActive;
e.skillPassive = h.skillPassive;
e.activeLv = Mathf.Min(Level(s, 0), Cap(db, h, s, 0));
e.passiveLv = Mathf.Min(Level(s, 1), Cap(db, h, s, 1));
e.passiveMult = PassiveMult(db, s != null ? s.star : h.rarity);
}
// ---------- 스킬서 임시 판매(거래 > 상점 · PD 확인 대상) ----------
public static int BooksBoughtToday(SaveData d, DateTime nowUtc)
{
return d.shop.bookDay == EconomyRules.DayKey(nowUtc) ? d.shop.bookBought : 0;
}
public static string CanBuyBook(OutGameData db, SaveData d, DateTime nowUtc)
{
var e = db.Economy;
if (BooksBoughtToday(d, nowUtc) >= e.SkillBookShopDaily) return "오늘 구매 " + e.SkillBookShopDaily + "회 완료";
if (d.gold < e.SkillBookPrice) return "골드 " + d.gold.ToString("N0") + " / " + e.SkillBookPrice.ToString("N0");
return null;
}
public static string BuyBook(OutGameData db, SaveData d, DateTime nowUtc)
{
var why = CanBuyBook(db, d, nowUtc);
if (why != null) return why;
var day = EconomyRules.DayKey(nowUtc);
if (d.shop.bookDay != day)
{
d.shop.bookDay = day;
d.shop.bookBought = 0;
}
d.shop.bookBought++;
d.gold -= db.Economy.SkillBookPrice;
d.inv.books.skill++;
return null;
}
}
}