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

374 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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