using System.Collections.Generic;
using UnityEngine;
namespace TKS
{
/// 피해 공식과 전장 기하 질의(경로 위 적·이동 가능 거리·최종 목표점 명중).
public static class Combat
{
/// 경로 질의에 쓰는 원 반지름 배율(살짝 작게 캐스트해 스침을 관대하게).
public const float CastRadiusScale = 0.9f;
public const float Epsilon = 0.02f;
/// 두 유닛 가장자리 간격이 이 이하면 「닿아 있다」(밀린 아군의 공격 기회 판정).
public const float ContactTolerance = 0.12f;
// ---------- 피해 공식 ----------
///
/// 피해 = (공격 × 상성 × 크리 − 방어 × defenseFactor) × 궁병 근접 배율 × 난수.
/// melee = 근접 전투(돌진·반격·관통·견제·밀려 닿은 공격). 궁병이 근접 유닛(보병·기병·창병)과 근접 전투하면
/// 주는 피해 × archerMeleeDealtRatio · 받는 피해 × archerMeleeTakenRatio (PD 2026-11-04 #1080) · 원거리 사격은 패널티 없음.
///
public static int Damage(BalanceConfig cfg, Unit attacker, Unit defender, bool crit, System.Random rng, bool melee)
{
float mult = cfg.Matchup(attacker.Type, defender.Type);
float raw = attacker.Atk * mult * (crit ? cfg.critMultiplier : 1f) - defender.Def * cfg.defenseFactor;
if (melee)
{
if (attacker.Type == UnitType.Archer && UnitTypeInfo.IsMelee(defender.Type)) raw *= cfg.archerMeleeDealtRatio;
if (defender.Type == UnitType.Archer && UnitTypeInfo.IsMelee(attacker.Type)) raw *= cfg.archerMeleeTakenRatio;
}
float variance = 1f + ((float)rng.NextDouble() * 2f - 1f) * cfg.damageVariance;
return Mathf.Max(1, Mathf.RoundToInt(raw * variance));
}
public static int CounterDamage(BalanceConfig cfg, Unit defender, Unit attacker, System.Random rng)
{
int baseDmg = Damage(cfg, defender, attacker, false, rng, true);
return Mathf.Max(1, Mathf.RoundToInt(baseDmg * cfg.counterRatio));
}
public static int OverwatchDamage(BalanceConfig cfg, Unit spear, Unit mover, System.Random rng)
{
int baseDmg = Damage(cfg, spear, mover, false, rng, true);
return Mathf.Max(1, Mathf.RoundToInt(baseDmg * cfg.overwatchDamageRatio));
}
// ---------- 기하 질의 ----------
static RaycastHit2D[] Cast(Unit actor, Vector2 origin, Vector2 dir, float dist)
{
Physics2D.SyncTransforms();
var hits = Physics2D.CircleCastAll(origin, actor.Radius * CastRadiusScale, dir, dist);
System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));
return hits;
}
static Unit UnitOf(Collider2D c)
{
return c != null ? c.GetComponent() : null;
}
/// 경로(원 캐스트) 위 가장 가까운 적. hitDist = 그 적에 닿기까지 이동 거리.
public static Unit FirstEnemyOnPath(Unit actor, Vector2 origin, Vector2 dir, float dist, out float hitDist)
{
hitDist = 0f;
foreach (var h in Cast(actor, origin, dir, dist))
{
var u = UnitOf(h.collider);
if (u == null || u == actor || !u.IsAlive || u.Faction == actor.Faction) continue;
hitDist = h.distance;
return u;
}
return null;
}
/// 경로(원 캐스트) 위 가장 가까운 유닛(아군·적군 불문). hitDist = 닿기까지 이동 거리. 벽은 무시한다(호출 쪽에서 FreeTravel 과 비교).
public static Unit FirstUnitOnPath(Unit actor, Vector2 origin, Vector2 dir, float dist, out float hitDist)
{
hitDist = 0f;
foreach (var h in Cast(actor, origin, dir, dist))
{
var u = UnitOf(h.collider);
if (u == null || u == actor || !u.IsAlive) continue;
hitDist = h.distance;
return u;
}
return null;
}
/// u 와 가장자리가 닿아 있는(ContactTolerance 이내) 가장 가까운 상대 진영 유닛.
public static Unit TouchingOpponent(Unit u)
{
Physics2D.SyncTransforms();
Unit best = null;
float bestGap = float.MaxValue;
foreach (var c in Physics2D.OverlapCircleAll(u.Position, u.Radius + ContactTolerance + 0.5f))
{
var o = UnitOf(c);
if (o == null || o == u || !o.IsAlive || o.Faction == u.Faction) continue;
float gap = Vector2.Distance(u.Position, o.Position) - u.Radius - o.Radius;
if (gap <= ContactTolerance && gap < bestGap)
{
bestGap = gap;
best = o;
}
}
return best;
}
/// 경로 위의 모든 적(가까운 순)과 각 접촉 거리.
public static List EnemiesOnPath(Unit actor, Vector2 origin, Vector2 dir, float dist, List distances)
{
var list = new List();
distances.Clear();
foreach (var h in Cast(actor, origin, dir, dist))
{
var u = UnitOf(h.collider);
if (u == null || u == actor || !u.IsAlive || u.Faction == actor.Faction) continue;
if (list.Contains(u)) continue;
list.Add(u);
distances.Add(h.distance);
}
return list;
}
/// 벽·(옵션) 유닛에 막히기 전까지 실제로 나아갈 수 있는 거리. ignore = 밀어 주는 유닛처럼 출발 지점에서 닿아 있어도 무시할 유닛.
public static float FreeTravel(Unit actor, Vector2 origin, Vector2 dir, float dist, bool enemiesBlock, bool friendsBlock = true, Unit ignore = null)
{
float min = dist;
foreach (var h in Cast(actor, origin, dir, dist))
{
if (h.collider.GetComponent() != null)
{
min = Mathf.Min(min, h.distance);
continue;
}
var u = UnitOf(h.collider);
if (u == null || u == actor || u == ignore || !u.IsAlive) continue;
bool enemy = u.Faction != actor.Faction;
if (enemy && !enemiesBlock) continue;
if (!enemy && !friendsBlock) continue;
min = Mathf.Min(min, h.distance);
}
return Mathf.Max(0f, min - Epsilon);
}
/// 목표점이 적 원 안(여유 포함)에 들어왔으면 그 적.
public static Unit EnemyAtPoint(Unit actor, Vector2 point, float tolerance)
{
Physics2D.SyncTransforms();
Unit best = null;
float bestD = float.MaxValue;
foreach (var c in Physics2D.OverlapCircleAll(point, tolerance))
{
var u = UnitOf(c);
if (u == null || u == actor || !u.IsAlive || u.Faction == actor.Faction) continue;
float d = Vector2.Distance(point, u.Position);
if (d < bestD)
{
bestD = d;
best = u;
}
}
return best;
}
/// 점 p 에서 가장 가까운 지정 진영 유닛(터치 여유 extra 포함).
public static Unit UnitNear(IEnumerable units, Vector2 p, Faction? faction, float extra)
{
Unit best = null;
float bestD = float.MaxValue;
foreach (var u in units)
{
if (!u.IsAlive) continue;
if (faction.HasValue && u.Faction != faction.Value) continue;
float d = Vector2.Distance(p, u.Position);
if (d <= u.Radius + extra && d < bestD)
{
bestD = d;
best = u;
}
}
return best;
}
/// 선분 ab 와 점 p 의 최단 거리.
public static float SegmentPointDistance(Vector2 a, Vector2 b, Vector2 p)
{
Vector2 ab = b - a;
float len2 = ab.sqrMagnitude;
if (len2 < 1e-6f) return Vector2.Distance(a, p);
float t = Mathf.Clamp01(Vector2.Dot(p - a, ab) / len2);
return Vector2.Distance(a + ab * t, p);
}
}
}