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);
}
}
}