using System.Collections.Generic; using UnityEngine; namespace TKS { /// /// 스킬 수치 질의 — ActionResolver · Combat · EnemyAI · BattleManager 가 수치를 읽을 때 부른다(시스템 기획 §5 · PD #1093 B). /// Get(unit, key) = 그 유닛에게 지금 걸린 효과(자기 패시브 · 같은 편 패시브(팀 키) · 장착된 예약 액티브 · 시한 효과). /// 스킬 시스템이 없거나 효과가 없으면 중립값(합 0 · 곱 1)이라 기존 규칙 그대로다. /// note = 실제 적용(피해·밀기) 때만 true → 패시브 발동 HUD 한 줄. /// public static class SkillMods { static readonly List Empty = new List(); public static List Get(Unit u, string key) { var sys = SkillSystem.Current; if (sys == null || u == null) return Empty; List list = null; foreach (var s in sys.Entries) { var src = s.Unit; if (src == null) continue; if (s.PassiveEffects != null && src.IsAlive) { foreach (var e in s.PassiveEffects) { if (e.Key != key || !Applies(src, u, e)) continue; if (list == null) list = new List(); list.Add(new SkillHit { Owner = s, Def = s.Passive, E = e, Passive = true }); } } if (s.Armed && src == u && s.ActiveEffects != null) { foreach (var e in s.ActiveEffects) { if (e.Key != key) continue; if (list == null) list = new List(); list.Add(new SkillHit { Owner = s, Def = s.Active, E = e, Passive = false }); } } } foreach (var t in sys.TimedEffects) { if (t.E.Key != key || !sys.WindowActive(t) || !TimedApplies(t, u)) continue; if (list == null) list = new List(); list.Add(new SkillHit { Owner = t.Owner, Def = t.Def, E = t.E, Passive = false }); } // 진영 상시 효과(보물 수집품 crit·range·splash · PD #1093 C) — 주인 없음 · HUD 한 줄 없음 var side = sys.Battle != null ? sys.Battle.SideEffects(u.Faction) : null; if (side != null) foreach (var e in side) { if (e.Key != key) continue; if (list == null) list = new List(); list.Add(new SkillHit { Owner = null, Def = null, E = e, Passive = false }); } return list ?? Empty; } /// 보물 전용 크리 피해 % 키(skills.csv 키 아님 · SkillKeys.All 에 넣지 않음 · PD #1093 C). public const string SideCritPct = "side_crit_pct"; /// 크리 배율 = 자산 critMultiplier × (1 + 보물 크리 %). public static float CritMult(Unit a, BalanceConfig cfg) { return cfg.critMultiplier * (1f + Sum(a, SideCritPct)); } static bool IsTeam(SkillEffect e) { SkillKeys.Info info; if (e.Key == SkillKeys.SlingDistPct) return e.HasA3 && e.A3 >= 0.5f; // p3 1 = 아군 전원 // 영웅 52명 확장(PD #1096): 기존 키의 조건 열로 아군 전원 — 원소 받는 피해(p3 출전 인원) · 순욱 반격(p3 1) · 순욱 최대 HP(p2 1) if (e.Key == SkillKeys.DmgTakenPct) return e.HasA3 && e.A3 > 0f; if (e.Key == SkillKeys.CounterRatio) return e.HasA3 && e.A3 >= 0.5f; if (e.Key == SkillKeys.HpMaxPct) return e.HasA2 && e.A2 >= 0.5f; if (e.Key == SkillKeys.CooldownAdd) return e.HasA2 && e.A2 >= 0.5f; return SkillKeys.All.TryGetValue(e.Key, out info) && info.Team; } static bool Applies(Unit src, Unit u, SkillEffect e) { if (!IsTeam(e)) return src == u; if (src.Faction != u.Faction) return false; if (e.Key == SkillKeys.TeamRangeAdd) return UnitTypeInfo.IsRanged(u.Type); if (e.Key == SkillKeys.DmgTakenAura) return Vector2.Distance(src.Position, u.Position) <= (e.HasA2 ? e.A2 : 0f) + u.Radius; return true; } static bool TimedApplies(SkillSystem.Timed t, Unit u) { if (t.Targets != null) return t.Targets.Contains(u); var src = t.Owner != null ? t.Owner.Unit : null; if (src == null) return false; if (!IsTeam(t.E)) return src == u; if (u.Faction != t.Faction) return false; if (t.E.Key == SkillKeys.DmgTakenAura) return src.IsAlive && Vector2.Distance(src.Position, u.Position) <= (t.E.HasA2 ? t.E.A2 : 0f) + u.Radius; return true; } static void Note(SkillHit h, bool note) { if (!note || !h.Passive || SkillSystem.Current == null) return; SkillSystem.Current.NotePassive(h.Owner, h.Def); } // ---------- 일반 ---------- public static float Sum(Unit u, string key, bool note = false) { float s = 0f; foreach (var h in Get(u, key)) { s += h.E.P; Note(h, note); } return s; } public static float Product(Unit u, string key, bool note = false) { float p = 1f; foreach (var h in Get(u, key)) { p *= h.E.P; Note(h, note); } return p; } /// Π(1 + 값) · 0 아래로 내려가지 않음(받는 피해 −% 류). public static float PctProduct(Unit u, string key, bool note = false) { float p = 1f; foreach (var h in Get(u, key)) { p *= Mathf.Max(0f, 1f + h.E.P); Note(h, note); } return p; } public static bool Has(Unit u, string key, bool note = false) { var l = Get(u, key); foreach (var h in l) Note(h, note); return l.Count > 0; } /// 횟수 = 걸린 효과 중 최댓값 · 없으면 fallback. public static int Count(Unit u, string key, int fallback, bool note = false) { var l = Get(u, key); if (l.Count == 0) return fallback; int n = int.MinValue; foreach (var h in l) { n = Mathf.Max(n, Mathf.RoundToInt(h.E.P)); Note(h, note); } return n; } // ---------- 슬링 거리 · 사거리 · 광역 ---------- /// 슬링 최대 거리 = 기본 × (1 + 비율 합) + 절대 가산(적토마 p2) — 동남풍(이번 턴 아군 전원) 포함. public static float SlingMax(Unit u, bool note = false) { if (u == null) return 0f; float pct = 0f; float add = 0f; foreach (var h in Get(u, SkillKeys.SlingDistPct)) { pct += h.E.P; if (h.E.HasA2) add += h.E.A2; Note(h, note); } return Mathf.Max(0.1f, u.SlingMaxDistance * (1f + pct) + add); } /// 궁병·책사 사거리 = (기본 + range_add + 대도독 team_range_add + 고지 p1(지형 기믹 PD #1097)) × range_mult. public static float Range(Unit u, bool note = false) { if (u == null) return 0f; float r = u.AttackRange + Sum(u, SkillKeys.RangeAdd, note) + Sum(u, SkillKeys.TeamRangeAdd, note) + GimmickLayer.RangeBonusOf(u); return Mathf.Max(0f, r * Product(u, SkillKeys.RangeMult, note)); } public static float SplashRadius(Unit u, BalanceConfig cfg, bool note = false) { return cfg.strategistSplashRadius * Product(u, SkillKeys.SplashRadiusMult, note); } /// 광역 비율 = 화공 치환값(여럿이면 큰 값) · 없으면 자산 값. public static float SplashRatio(Unit u, BalanceConfig cfg, bool note = false) { var l = Get(u, SkillKeys.SplashRatio); if (l.Count == 0) return cfg.strategistSplashRatio; float r = float.MinValue; foreach (var h in l) { r = Mathf.Max(r, h.E.P); Note(h, note); } return r; } /// 광역 아군 비율 = 팔문 간파 치환값(여럿이면 작은 값) · 없으면 자산 값. public static float SplashAllyRatio(Unit u, BalanceConfig cfg, bool note = false) { var l = Get(u, SkillKeys.SplashAllyRatio); if (l.Count == 0) return cfg.strategistAllyDamageRatio; float r = float.MaxValue; foreach (var h in l) { r = Mathf.Min(r, h.E.P); Note(h, note); } return Mathf.Max(0f, r); } // ---------- 피해 ---------- /// 상성 하한(대부 · 0 = 없음). public static float MatchupFloor(Unit a, bool note = false) { float f = 0f; foreach (var h in Get(a, SkillKeys.MatchupFloor)) { f = Mathf.Max(f, h.E.P); Note(h, note); } return f; } /// 공격력 배율 = 1 + 「공 +%」 합(노익장·맹장 HP 조건 · 선봉 1턴 · 고육계 · 복수 · 패자 · 팔백 돌격). public static float AttackMult(Unit a, bool note = false) { var sys = SkillSystem.Current; if (sys == null || a == null) return 1f; float pct = 0f; foreach (var h in Get(a, SkillKeys.DmgPctHpBelow)) { float th = h.E.HasA2 ? h.E.A2 : 0f; if (a.Hp > a.MaxHp * th) continue; pct += h.E.P; Note(h, note); } if (sys.Battle.TurnNumber == 1) pct += Sum(a, SkillKeys.DmgPctTurn1, note); pct += Sum(a, SkillKeys.HpCostAtk, note); var deaths = Get(a, SkillKeys.OnAllyDeathAtk); if (deaths.Count > 0) { int dead = DeadAllies(a); if (dead > 0) foreach (var h in deaths) { pct += h.E.P * dead; Note(h, note); } } foreach (var h in Get(a, SkillKeys.TeamAtkPctFull)) { int need = h.E.HasA2 ? Mathf.RoundToInt(h.E.A2) : 0; if (sys.StartCount(a.Faction) < need) continue; pct += h.E.P; Note(h, note); } foreach (var h in Get(a, SkillKeys.PierceDmgPctPerHit)) { var s = sys.Of(a); int hits = s != null ? s.PierceHits : 0; if (hits <= 0) continue; float v = h.E.P * hits; if (h.E.HasA2) v = Mathf.Min(v, h.E.A2); pct += v; Note(h, note); } return Mathf.Max(0f, 1f + pct); } static int DeadAllies(Unit a) { var sys = SkillSystem.Current; if (sys == null) return 0; int n = 0; foreach (var u in sys.Battle.Units) if (u != null && u != a && u.Faction == a.Faction && !u.IsAlive) n++; return n; } /// 주는 피해 배율 = (1 + 알까기 피해 %) × 무장 대상 배율(참장 = 영웅·보스 · 파로 = 보스만). public static float DealtMult(Unit a, Unit d, bool note = false) { var sys = SkillSystem.Current; float m = 1f + Sum(a, SkillKeys.TeamDmgPctTurn, note); // 십승십패·사세삼공(이번 턴 아군 전원) foreach (var h in Get(a, SkillKeys.DmgPct)) { // 키 하나인 행의 p2 = 발동 턴(이상 · 여몽 5턴째부터) if (h.E.HasA2 && h.E.A2 >= 1f && sys != null && sys.Battle.TurnNumber < Mathf.RoundToInt(h.E.A2)) continue; m += h.E.P; Note(h, note); } foreach (var h in Get(a, SkillKeys.DmgVsGeneralMult)) { bool bossOnly = h.E.HasA2 && h.E.A2 >= 0.5f; bool ok = bossOnly ? d.IsBoss : (sys != null && sys.IsGeneral(d)); if (!ok) continue; m *= h.E.P; Note(h, note); } return Mathf.Max(0f, m); } /// 받는 피해 배율 = 수성·노장 × 간웅(이번 적 턴) × 철벽(반경) × 적 궁병 피해 감소. public static float TakenMult(Unit d, Unit a, bool note = false) { float m = DmgTakenPct(d, a, note) * PctProduct(d, SkillKeys.TeamDmgTakenPctTurn, note) * PctProduct(d, SkillKeys.DmgTakenAura, note); if (a != null && a.Type == UnitType.Archer) m *= PctProduct(d, SkillKeys.DmgTakenFromArcherPct, note); return m; } /// /// 받는 피해 % (dmg_taken_pct) — 키 하나인 행의 조건(영웅 52명 확장 · PD #1096): p2 0~1 = HP 그 비율 이하일 때(위연) · p2 2 = 보스에게서만(원술) · /// p2 3 = 근접 유닛에게서만(초선) · p3 n = 출전 n 인 이상일 때 아군 전원(원소). 조건 열이 없으면 기존 그대로(조인·황개). /// static float DmgTakenPct(Unit d, Unit a, bool note) { var sys = SkillSystem.Current; float p = 1f; foreach (var h in Get(d, SkillKeys.DmgTakenPct)) { if (h.E.HasA2 && h.E.A2 > 0f) { float c = h.E.A2; if (c < 1f && d.Hp > d.MaxHp * c) continue; if (Mathf.Approximately(c, 2f) && (a == null || !a.IsBoss)) continue; if (Mathf.Approximately(c, 3f) && (a == null || !UnitTypeInfo.IsMelee(a.Type))) continue; } if (h.E.HasA3 && h.E.A3 > 0f && (sys == null || sys.StartCount(d.Faction) < Mathf.RoundToInt(h.E.A3))) continue; p *= Mathf.Max(0f, 1f + h.E.P); Note(h, note); } return p; } /// 반격 배율 = 무성·무언의 규율(치환) · 진중(p2 1 = 밀렸을 때만). public static float CounterRatio(Unit counterer, float baseRatio, bool pushed, bool note = false) { float r = baseRatio; foreach (var h in Get(counterer, SkillKeys.CounterRatio)) { if (h.E.HasA2 && h.E.A2 >= 0.5f && !pushed) continue; if (h.E.P <= r) continue; r = h.E.P; Note(h, note); } return r; } // ---------- 밀기 · 튕김 · 관통 ---------- public static float PushMult(Unit actor, bool note = false) { return Mathf.Max(0f, Product(actor, SkillKeys.PushMult, note)); } /// 함진영: 밀린 적이 벽·유닛에 닿으면 추가 피해 비율(push_mult 행의 p2). public static float PushCollisionBonus(Unit actor) { float b = 0f; foreach (var h in Get(actor, SkillKeys.PushMult)) if (h.E.HasA2) b += h.E.A2; return b; } public static float SelfBounceMult(Unit actor, bool note = false) { return Mathf.Max(0f, Product(actor, SkillKeys.SelfBounceMult, note)); } /// 밀리는 거리 배율(거구 −50 %). public static float KnockbackTakenMult(Unit target, bool note = false) { return PctProduct(target, SkillKeys.KnockbackTakenPct, note); } public static float PierceKnockbackMult(Unit actor, bool note = false) { return Mathf.Max(0f, Product(actor, SkillKeys.PierceKnockbackMult, note)); } /// 관통 피해 배율 · 키 하나인 행의 p2 = 최소 관통 수(만인지적 2기 이상). public static float PierceDmgMult(Unit actor, int pierced, bool note = false) { float m = 1f; foreach (var h in Get(actor, SkillKeys.PierceDmgMult)) { if (h.E.HasA2 && pierced < Mathf.RoundToInt(h.E.A2)) continue; m *= h.E.P; Note(h, note); } return Mathf.Max(0f, m); } // ---------- 견제 ---------- /// 견제 반경 = (기본 + 후계 등 패시브 가산 + 이번 대기의 액티브 가산) × 액티브 배율(청룡언월). public static float OverwatchRadius(Unit spear, bool note = false) { if (spear == null) return 0f; var s = SkillSystem.Current != null ? SkillSystem.Current.Of(spear) : null; float add = Sum(spear, SkillKeys.OverwatchRadiusAdd, note) + (s != null ? s.OwAdd : 0f); float mult = s != null ? s.OwMult : 1f; return Mathf.Max(0f, (spear.AttackRange + add) * mult); } public static bool OverwatchUnlimited(Unit spear) { var s = SkillSystem.Current != null ? SkillSystem.Current.Of(spear) : null; return (s != null && s.OwUnlimited) || Has(spear, SkillKeys.OverwatchUnlimited); } } }