using System.Collections.Generic;
using UnityEngine;
namespace TKS
{
///
/// 스킬 수치 질의(2026-11-10 #1112 · 효과 표 skill_effects.csv) — ActionResolver · Combat · EnemyAI · BattleManager · SlingshotInput 이 수치를 읽을 때 부른다.
/// Get(unit, key, other, crit) = 그 유닛에게 지금 걸린 효과: 패시브(대상 칸 = 자신 · 같은 편 · 진영 · 반경 · 슬링 범위) · 장착된 예약 액티브 · 시한 효과 · 보물 진영 효과.
/// 조건 칸(치명타 · 상대 상태 이상 · 병종 · 성별 · 턴 · 장착 중)은 other(공격 대상 또는 공격자)·crit 으로 판정한다. 효과가 없으면 중립값(합 0 · 곱 1).
/// note = 실제 적용 때만 true → 패시브 발동 HUD 한 줄.
///
public static class SkillMods
{
static readonly List Empty = new List();
public static List Get(Unit u, string key, Unit other = null, bool crit = false)
{
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 || s.BannerLeft > 0))
{
foreach (var e in s.PassiveEffects)
{
if (e.Key != key || !Applies(s, src, u, e, false) || !CondOk(sys, s, e, u, other, crit)) continue;
if (list == null) list = new List();
list.Add(new SkillHit { Owner = s, Def = s.Passive, E = e, Passive = true });
}
}
if ((s.Armed || s.Casting) && src == u && s.ActiveEffects != null)
{
foreach (var e in s.ActiveEffects)
{
if (e.Key != key || e.Key == SkillKeys.Invuln || !CondOk(sys, s, e, u, other, crit)) continue;
if (e.Scope != SkillScope.Self && e.Dur > 0) 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 || !TimedApplies(t, u) || !CondOk(sys, t.Owner, t.E, u, other, crit)) continue;
if (list == null) list = new List();
list.Add(new SkillHit { Owner = t.Owner, Def = t.Def, E = t.E, Passive = false, Timed = t });
}
// 진영 상시 효과(보물 수집품 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 표 키 아님 · SkillKeys.All 에 넣지 않음 · PD #1093 C).
public const string SideCritPct = "side_crit_pct";
/// 「행동 반경」 = 주인의 기본 슬링 최대 거리(효과로 늘어난 값이 아님 — 질의가 서로 부르지 않게).
public static float BaseReach(Unit owner)
{
return owner != null ? owner.SlingMaxDistance : 0f;
}
public static bool Within(Unit owner, Unit u, float r)
{
return owner != null && u != null && Vector2.Distance(owner.Position, u.Position) <= r + u.Radius;
}
/// 대상 칸 판정(dynamic = 매번 · Range 는 시한 효과에서 고정 대상으로 처리).
public static bool Applies(UnitSkills s, Unit src, Unit u, SkillEffect e, bool timed)
{
if (e.Scope == SkillScope.Self) return src == u;
if (src.Faction != u.Faction) return false;
switch (e.Scope)
{
case SkillScope.Allies: return true;
case SkillScope.Camp: return u.Camp == e.Camp && (!e.HasType || u.Type == e.Type);
case SkillScope.CampMale: return u.Camp == e.Camp && !u.Female;
case SkillScope.Radius: return u != src && Within(src, u, e.Radius);
case SkillScope.Range:
case SkillScope.Aura:
if (!src.IsAlive && s != null && s.BannerLeft > 0) return Vector2.Distance(s.BannerPos, u.Position) <= BaseReach(src) + u.Radius;
return Within(src, u, BaseReach(src));
}
return false;
}
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 (t.E.Scope != SkillScope.Self && !src.IsAlive) return false;
return Applies(t.Owner, src, u, t.E, true);
}
/// 조건 칸 — self = 효과를 받는 유닛 · other = 상대(공격 대상 또는 공격자).
static bool CondOk(SkillSystem sys, UnitSkills owner, SkillEffect e, Unit self, Unit other, bool crit)
{
switch (e.Cond)
{
case SkillCondKind.None: return true;
case SkillCondKind.Crit: return crit;
case SkillCondKind.TurnLe: return sys.Battle != null && sys.Battle.TurnNumber <= e.CondN;
case SkillCondKind.Armed: return owner != null && (owner.Armed || owner.Casting);
}
if (other == null) return false;
switch (e.Cond)
{
case SkillCondKind.VsStatus: return SkillStatus.HasAny(other);
case SkillCondKind.VsBleed: return SkillStatus.Bleeding(other);
case SkillCondKind.VsConfused: return SkillStatus.Confused(other);
case SkillCondKind.VsStunned: return SkillStatus.Stunned(other);
case SkillCondKind.VsType: return other.Type == e.CondType;
case SkillCondKind.VsMale: return !other.Female;
case SkillCondKind.VsMaleHero: return !other.Female && sys.IsGeneral(other);
case SkillCondKind.VsFemaleHero: return other.Female && sys.IsGeneral(other);
}
return false;
}
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, Unit other = null, bool crit = false)
{
float s = 0f;
foreach (var h in Get(u, key, other, crit))
{
s += h.E.P;
Note(h, note);
}
return s;
}
public static float Product(Unit u, string key, bool note = false, Unit other = null)
{
float p = 1f;
foreach (var h in Get(u, key, other))
{
p *= h.E.P;
Note(h, note);
}
return p;
}
/// Π(1 + 값) · 0 아래로 내려가지 않음(받는 피해 −% 류).
public static float PctProduct(Unit u, string key, bool note = false, Unit other = null)
{
float p = 1f;
foreach (var h in Get(u, key, other))
{
p *= Mathf.Max(0f, 1f + h.E.P);
Note(h, note);
}
return p;
}
public static bool Has(Unit u, string key, bool note = false, Unit other = null, bool crit = false)
{
var l = Get(u, key, other, crit);
foreach (var h in l) Note(h, note);
return l.Count > 0;
}
/// 첫 효과(수치 칸을 그대로 쓰는 처리기) · 없으면 null.
public static SkillEffect First(Unit u, string key, bool note = false)
{
var l = Get(u, key);
if (l.Count == 0) return null;
Note(l[0], note);
return l[0].E;
}
// ---------- 슬링 거리 · 사거리 · 광역 ----------
///
/// 슬링 최대 거리 = 기본 × (1 + 이동 % 합) × (1 + 진중 축적) × 혼란 배율 · 재행동 남은 거리 상한(감녕·고순) — 조준 · 실제 이동 · 적 AI 공용.
///
public static float SlingMax(Unit u, bool note = false)
{
if (u == null) return 0f;
float pct = Sum(u, SkillKeys.SlingDistPct, note);
var sys = SkillSystem.Current;
var s = sys != null ? sys.Of(u) : null;
float bank = s != null ? s.Bank : 0f;
float v = u.SlingMaxDistance * Mathf.Max(0.1f, 1f + pct) * (1f + bank) * SkillStatus.ConfuseMult(u);
if (s != null && s.ReactLimit > 0f) v = Mathf.Min(v, s.ReactLimit);
return Mathf.Max(0.1f, v);
}
/// 궁병·책사 사거리 = (기본 + 고지 p1(PD #1097)) × range_mult(대상·조건 칸 · 보물) · 근접 병종이 원거리로 쏠 때(축융 비도·주태 검기) = 슬링 거리 기준.
public static float Range(Unit u, bool note = false)
{
if (u == null) return 0f;
float b = UnitTypeInfo.IsRanged(u.Type) ? u.AttackRange : u.SlingMaxDistance;
float r = b + 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)
{
return cfg.strategistSplashRatio;
}
/// 광역 아군 비율 = 서서·제갈근 치환값(여럿이면 작은 값) · 없으면 자산 값.
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);
}
// ---------- 피해 ----------
/// 상성 하한(불리 상성 무시 = 1 · 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 + 공 % 합(조건 = 상대 d) + 처치 누적 + 잃은 HP + 범위 안 아군 + 아군 전사 + 돌진 거리 + 재행동 누적 + 고육계 이월) × 「공격력의 X %」(atk_ratio) × 저HP 스킬 위력(주유).
///
public static float AttackMult(Unit a, bool note = false, Unit d = null)
{
var sys = SkillSystem.Current;
if (sys == null || a == null) return 1f;
var s = sys.Of(a);
float pct = Sum(a, SkillKeys.AtkPct, note, d);
foreach (var h in Get(a, SkillKeys.KillStackAtk))
{
int kills = s != null ? s.Kills : 0;
if (kills <= 0) continue;
pct += Mathf.Min(h.E.HasA2 ? h.E.A2 : float.MaxValue, h.E.P * kills);
Note(h, note);
}
float missing = a.MaxHp > 0 ? Mathf.Clamp01(1f - (float)a.Hp / a.MaxHp) : 0f;
foreach (var h in Get(a, SkillKeys.AtkMissingHp))
{
if (missing <= 0f) continue;
pct += Mathf.Min(h.E.HasA2 && h.E.A2 > 0f ? h.E.A2 : float.MaxValue, h.E.P * missing);
Note(h, note);
}
foreach (var h in Get(a, SkillKeys.AtkPerAllyRange))
{
int n = 0;
foreach (var o in sys.Battle.AliveUnits(a.Faction))
if (o != a && Within(a, o, BaseReach(a))) n++;
if (n == 0) continue;
pct += h.E.P * n;
Note(h, 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);
}
}
var ctx = sys.Ctx;
if (ctx != null && ctx.Actor == a && ctx.SlingRatio > 0f)
foreach (var h in Get(a, SkillKeys.DashDmgPct))
{
pct += h.E.P * ctx.SlingRatio;
Note(h, note);
}
if (s != null && s.ReactsThisTurn > 0)
foreach (var h in Get(a, SkillKeys.ReactionAtkStack))
{
pct += h.E.P * Mathf.Pow(2f, s.ReactsThisTurn - 1);
Note(h, note);
}
if (s != null && s.AtkFlat > 0f && a.Atk > 0) pct += s.AtkFlat / a.Atk; // 고육계: 지난 턴 받은 피해 → 이번 턴 공격력
float m = Mathf.Max(0f, 1f + pct);
m *= Product(a, SkillKeys.AtkRatio, note);
if (s != null && (s.Armed || s.Casting))
foreach (var h in Get(a, SkillKeys.SkillPowerLowHp))
{
float step = h.E.HasA2 && h.E.A2 > 0f ? h.E.A2 : 0.1f;
int k = Mathf.FloorToInt(missing / step + 1e-4f);
if (k <= 0) continue;
m *= 1f + h.E.P * k;
Note(h, note);
}
return m;
}
public 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 && !SkillSystem.IsSummon(u)) n++;
return n;
}
/// 주는 피해 배율 = 1 + 피해 % 합(조건 = 상대 d) + 마초 「맞힌 적 1기당」.
public static float DealtMult(Unit a, Unit d, bool note = false)
{
var sys = SkillSystem.Current;
float m = 1f + Sum(a, SkillKeys.DmgPct, note, d);
var ctx = sys != null ? sys.Ctx : null;
if (ctx != null && ctx.Actor == a && ctx.HitCount > 0)
foreach (var h in Get(a, SkillKeys.DmgPerHit))
{
m += h.E.P * ctx.HitCount;
Note(h, note);
}
return Mathf.Max(0f, m);
}
/// 받는 피해 배율 = 받는 피해 %(조건 = 공격자 a) × 원거리 받는 피해 × 적 궁병 피해 × 수성(남은 이동 거리) × 노장(잃은 HP).
public static float TakenMult(Unit d, Unit a, bool note = false, bool ranged = false)
{
float m = PctProduct(d, SkillKeys.DmgTakenPct, note, a);
if (ranged) m *= PctProduct(d, SkillKeys.DmgTakenRangedPct, note, a);
if (a != null && a.Type == UnitType.Archer) m *= PctProduct(d, SkillKeys.DmgTakenFromArcherPct, note, a);
var sys = SkillSystem.Current;
var s = sys != null ? sys.Of(d) : null;
if (s != null && s.UnusedMove > 0f)
foreach (var h in Get(d, SkillKeys.UnusedMoveDr))
{
m *= Mathf.Max(0f, 1f - h.E.P * s.UnusedMove);
Note(h, note);
}
float lost = d.MaxHp > 0 ? Mathf.Clamp01(1f - (float)d.Hp / d.MaxHp) * 100f : 0f;
if (lost > 0f)
foreach (var h in Get(d, SkillKeys.LowHpTaken))
{
m *= 1f + h.E.P * lost;
Note(h, note);
}
return m;
}
// ---------- 치명타 ----------
/// 크리 배율 = 자산 critMultiplier × (1 + 보물 크리 %) × (1 + 치명타 피해 % 합).
public static float CritMult(Unit a, BalanceConfig cfg, Unit d = null)
{
return cfg.critMultiplier * (1f + Sum(a, SideCritPct)) * (1f + Sum(a, SkillKeys.CritDmgPct, true, d));
}
public static float CritChance(Unit a, Unit d)
{
return Mathf.Clamp01(Sum(a, SkillKeys.CritChance, false, d));
}
public static bool CritAlways(Unit a, Unit d)
{
return Has(a, SkillKeys.CritAlways, true, d);
}
// ---------- 반격 ----------
/// 받는 반격 피해 배율(공격한 유닛 a 기준 · 조건 = 반격하는 상대) · 0 = 반격 안 받음.
public static float CounterTakenMult(Unit a, Unit counterer, bool note = false)
{
return PctProduct(a, SkillKeys.CounterTakenPct, note, counterer);
}
/// 주는 반격 피해 배율 = 1 + 반격 피해 % 합 + 노장(잃은 HP 1 %당 p2).
public static float CounterDmgMult(Unit counterer, bool note = false)
{
float m = 1f + Sum(counterer, SkillKeys.CounterDmgPct, note);
float lost = counterer.MaxHp > 0 ? Mathf.Clamp01(1f - (float)counterer.Hp / counterer.MaxHp) * 100f : 0f;
if (lost > 0f)
foreach (var h in Get(counterer, SkillKeys.LowHpTaken))
if (h.E.HasA2) m += h.E.A2 * lost;
return Mathf.Max(0f, m);
}
// ---------- 밀기 · 튕김 ----------
public static float PushMult(Unit actor, bool note = false)
{
return Mathf.Max(0f, Product(actor, SkillKeys.PushMult, note));
}
public static float SelfBounceMult(Unit actor, bool note = false)
{
return Mathf.Max(0f, Product(actor, SkillKeys.SelfBounceMult, note));
}
/// 밀리는 거리 배율(거구 −100 % = 안 밀림).
public static float KnockbackTakenMult(Unit target, bool note = false)
{
return PctProduct(target, SkillKeys.KnockbackTakenPct, note);
}
/// 서황 대부: 도끼가 도는 동안 자신을 공격한 유닛은 × p3 멀리 튕긴다.
public static float AxeBounce(Unit struck)
{
if (struck == null) return 1f;
float m = 1f;
foreach (var h in Get(struck, SkillKeys.AxeAura))
if (h.E.HasA3) m = Mathf.Max(m, h.E.A3);
return m;
}
/// 전위 고지악래: 지형 오브젝트 밀기 거리·그것으로 준 피해 배율.
public static float GimmickMult(Unit u)
{
return u != null ? Mathf.Max(0f, Product(u, SkillKeys.GimmickMult, true)) : 1f;
}
}
}