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

444 lines
20 KiB
C#
Raw Permalink 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>
/// 스킬 수치 질의(2026-11-10 #1112 · 효과 표 skill_effects.csv) — ActionResolver · Combat · EnemyAI · BattleManager · SlingshotInput 이 수치를 읽을 때 부른다.
/// Get(unit, key, other, crit) = 그 유닛에게 지금 걸린 효과: 패시브(대상 칸 = 자신 · 같은 편 · 진영 · 반경 · 슬링 범위) · 장착된 예약 액티브 · 시한 효과 · 보물 진영 효과.
/// 조건 칸(치명타 · 상대 상태 이상 · 병종 · 성별 · 턴 · 장착 중)은 other(공격 대상 또는 공격자)·crit 으로 판정한다. 효과가 없으면 중립값(합 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, Unit other = null, bool crit = false)
{
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 || 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<SkillHit>();
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<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 || !TimedApplies(t, u) || !CondOk(sys, t.Owner, t.E, u, other, crit)) continue;
if (list == null) list = new List<SkillHit>();
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<SkillHit>();
list.Add(new SkillHit { Owner = null, Def = null, E = e, Passive = false });
}
return list ?? Empty;
}
/// <summary>보물 전용 크리 피해 % 키(skills 표 키 아님 · SkillKeys.All 에 넣지 않음 · PD #1093 C).</summary>
public const string SideCritPct = "side_crit_pct";
/// <summary>「행동 반경」 = 주인의 기본 슬링 최대 거리(효과로 늘어난 값이 아님 — 질의가 서로 부르지 않게).</summary>
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;
}
/// <summary>대상 칸 판정(dynamic = 매번 · Range 는 시한 효과에서 고정 대상으로 처리).</summary>
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);
}
/// <summary>조건 칸 — self = 효과를 받는 유닛 · other = 상대(공격 대상 또는 공격자).</summary>
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;
}
/// <summary>Π(1 + 값) · 0 아래로 내려가지 않음(받는 피해 −% 류).</summary>
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;
}
/// <summary>첫 효과(수치 칸을 그대로 쓰는 처리기) · 없으면 null.</summary>
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;
}
// ---------- 슬링 거리 · 사거리 · 광역 ----------
/// <summary>
/// 슬링 최대 거리 = 기본 × (1 + 이동 % 합) × (1 + 진중 축적) × 혼란 배율 · 재행동 남은 거리 상한(감녕·고순) — 조준 · 실제 이동 · 적 AI 공용.
/// </summary>
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);
}
/// <summary>궁병·책사 사거리 = (기본 + 고지 p1(PD #1097)) × range_mult(대상·조건 칸 · 보물) · 근접 병종이 원거리로 쏠 때(축융 비도·주태 검기) = 슬링 거리 기준.</summary>
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;
}
/// <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>상성 하한(불리 상성 무시 = 1 · 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 + 공 % 합(조건 = 상대 d) + 처치 누적 + 잃은 HP + 범위 안 아군 + 아군 전사 + 돌진 거리 + 재행동 누적 + 고육계 이월) × 「공격력의 X %」(atk_ratio) × 저HP 스킬 위력(주유).
/// </summary>
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;
}
/// <summary>주는 피해 배율 = 1 + 피해 % 합(조건 = 상대 d) + 마초 「맞힌 적 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, 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);
}
/// <summary>받는 피해 배율 = 받는 피해 %(조건 = 공격자 a) × 원거리 받는 피해 × 적 궁병 피해 × 수성(남은 이동 거리) × 노장(잃은 HP).</summary>
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;
}
// ---------- 치명타 ----------
/// <summary>크리 배율 = 자산 critMultiplier × (1 + 보물 크리 %) × (1 + 치명타 피해 % 합).</summary>
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);
}
// ---------- 반격 ----------
/// <summary>받는 반격 피해 배율(공격한 유닛 a 기준 · 조건 = 반격하는 상대) · 0 = 반격 안 받음.</summary>
public static float CounterTakenMult(Unit a, Unit counterer, bool note = false)
{
return PctProduct(a, SkillKeys.CounterTakenPct, note, counterer);
}
/// <summary>주는 반격 피해 배율 = 1 + 반격 피해 % 합 + 노장(잃은 HP 1 %당 p2).</summary>
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));
}
/// <summary>밀리는 거리 배율(거구 −100 % = 안 밀림).</summary>
public static float KnockbackTakenMult(Unit target, bool note = false)
{
return PctProduct(target, SkillKeys.KnockbackTakenPct, note);
}
/// <summary>서황 대부: 도끼가 도는 동안 자신을 공격한 유닛은 × p3 멀리 튕긴다.</summary>
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;
}
/// <summary>전위 고지악래: 지형 오브젝트 밀기 거리·그것으로 준 피해 배율.</summary>
public static float GimmickMult(Unit u)
{
return u != null ? Mathf.Max(0f, Product(u, SkillKeys.GimmickMult, true)) : 1f;
}
}
}