374 lines
15 KiB
C#
374 lines
15 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace TKS
|
||
{
|
||
/// <summary>
|
||
/// 스킬 수치 질의 — ActionResolver · Combat · EnemyAI · BattleManager 가 수치를 읽을 때 부른다(시스템 기획 §5 · PD #1093 B).
|
||
/// Get(unit, key) = 그 유닛에게 지금 걸린 효과(자기 패시브 · 같은 편 패시브(팀 키) · 장착된 예약 액티브 · 시한 효과).
|
||
/// 스킬 시스템이 없거나 효과가 없으면 중립값(합 0 · 곱 1)이라 기존 규칙 그대로다.
|
||
/// note = 실제 적용(피해·밀기) 때만 true → 패시브 발동 HUD 한 줄.
|
||
/// </summary>
|
||
public static class SkillMods
|
||
{
|
||
static readonly List<SkillHit> Empty = new List<SkillHit>();
|
||
|
||
public static List<SkillHit> Get(Unit u, string key)
|
||
{
|
||
var sys = SkillSystem.Current;
|
||
if (sys == null || u == null) return Empty;
|
||
List<SkillHit> 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<SkillHit>();
|
||
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<SkillHit>();
|
||
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<SkillHit>();
|
||
list.Add(new SkillHit { Owner = t.Owner, Def = t.Def, E = t.E, Passive = false });
|
||
}
|
||
return list ?? Empty;
|
||
}
|
||
|
||
static bool IsTeam(SkillEffect e)
|
||
{
|
||
SkillKeys.Info info;
|
||
if (e.Key == SkillKeys.SlingDistPct) return e.HasA3 && e.A3 >= 0.5f; // p3 1 = 아군 전원
|
||
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;
|
||
}
|
||
|
||
/// <summary>Π(1 + 값) · 0 아래로 내려가지 않음(받는 피해 −% 류).</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>횟수 = 걸린 효과 중 최댓값 · 없으면 fallback.</summary>
|
||
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;
|
||
}
|
||
|
||
// ---------- 슬링 거리 · 사거리 · 광역 ----------
|
||
|
||
/// <summary>슬링 최대 거리 = 기본 × (1 + 비율 합) + 절대 가산(적토마 p2) — 동남풍(이번 턴 아군 전원) 포함.</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>궁병·책사 사거리 = (기본 + range_add + 대도독 team_range_add) × range_mult.</summary>
|
||
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);
|
||
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);
|
||
}
|
||
|
||
/// <summary>광역 비율 = 화공 치환값(여럿이면 큰 값) · 없으면 자산 값.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>광역 아군 비율 = 팔문 간파 치환값(여럿이면 작은 값) · 없으면 자산 값.</summary>
|
||
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);
|
||
}
|
||
|
||
// ---------- 피해 ----------
|
||
|
||
/// <summary>상성 하한(대부 · 0 = 없음).</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>공격력 배율 = 1 + 「공 +%」 합(노익장·맹장 HP 조건 · 선봉 1턴 · 고육계 · 복수 · 패자 · 팔백 돌격).</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>주는 피해 배율 = (1 + 알까기 피해 %) × 무장 대상 배율(참장 = 영웅·보스 · 파로 = 보스만).</summary>
|
||
public static float DealtMult(Unit a, Unit d, bool note = false)
|
||
{
|
||
var sys = SkillSystem.Current;
|
||
float m = 1f + Sum(a, SkillKeys.DmgPct, 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);
|
||
}
|
||
|
||
/// <summary>받는 피해 배율 = 수성·노장 × 간웅(이번 적 턴) × 철벽(반경) × 적 궁병 피해 감소.</summary>
|
||
public static float TakenMult(Unit d, Unit a, bool note = false)
|
||
{
|
||
float m = PctProduct(d, SkillKeys.DmgTakenPct, 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;
|
||
}
|
||
|
||
/// <summary>반격 배율 = 무성·무언의 규율(치환) · 진중(p2 1 = 밀렸을 때만).</summary>
|
||
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));
|
||
}
|
||
|
||
/// <summary>함진영: 밀린 적이 벽·유닛에 닿으면 추가 피해 비율(push_mult 행의 p2).</summary>
|
||
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));
|
||
}
|
||
|
||
/// <summary>밀리는 거리 배율(거구 −50 %).</summary>
|
||
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));
|
||
}
|
||
|
||
/// <summary>관통 피해 배율 · 키 하나인 행의 p2 = 최소 관통 수(만인지적 2기 이상).</summary>
|
||
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);
|
||
}
|
||
|
||
// ---------- 견제 ----------
|
||
|
||
/// <summary>견제 반경 = (기본 + 후계 등 패시브 가산 + 이번 대기의 액티브 가산) × 액티브 배율(청룡언월).</summary>
|
||
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);
|
||
}
|
||
}
|
||
}
|