using System.Collections.Generic; using UnityEngine; namespace TKS { /// /// 적군 슬링샷 결정: 교전 범위 안의 가장 가까운 아군을 향해 당기고, 정확도에 따라 흔들린다. 범위 밖이면 대기(null). /// 궁병·책사(PD 2026-11-05 #1087 ⑤⑥ · cfg 를 넘기면): 공격 가능 범위 안에서 사선이 빈 대상 우선 · 책사는 맞을 자리 광역에 자기 편이 없는 대상을 그보다 먼저 · 없으면 가장 가까운 대상(그냥 쏜다). /// public static class EnemyAI { public static Vector2? Decide(Unit e, IReadOnlyList targets, float accuracy, float aggroRange, System.Random rng, Board board, BalanceConfig cfg = null) { Unit nearest = null; float best = float.MaxValue; for (int i = 0; i < targets.Count; i++) { var t = targets[i]; if (!t.IsAlive) continue; float d = Vector2.Distance(e.Position, t.Position); if (d < best) { best = d; nearest = t; } } if (nearest == null) return null; if (best > aggroRange) return null; // 넓은 전장: 교전 범위 밖이면 자리 지킴 if (cfg != null && UnitTypeInfo.IsRanged(e.Type)) nearest = PickRangedTarget(e, targets, nearest, cfg); Vector2 toT = nearest.Position - e.Position; float dist = toT.magnitude; Vector2 dir = dist > 0.001f ? toT / dist : Vector2.up; float sling = SkillMods.SlingMax(e); // 적 무장 패시브(적토마 등 · PD #1093 B) 반영 Vector2 target; if (dist <= sling) { target = nearest.Position; // 최종 목표점 명중 시도 } else { target = e.Position + dir * sling; // 접근(궁병은 사거리 안이면 이동 후 사격) } if (rng.NextDouble() > accuracy) { float ang = ((float)rng.NextDouble() * 2f - 1f) * 22f * Mathf.Deg2Rad; float cs = Mathf.Cos(ang); float sn = Mathf.Sin(ang); Vector2 rd = new Vector2(dir.x * cs - dir.y * sn, dir.x * sn + dir.y * cs); float len = (target - e.Position).magnitude * (0.75f + 0.5f * (float)rng.NextDouble()); target = e.Position + rd * Mathf.Min(len, sling); } Vector2 v = target - e.Position; if (v.magnitude > sling) v = v.normalized * sling; return board.ClampInside(e.Position + v, e.Radius); } /// /// 원거리 대상 고르기: 공격 가능 범위(사거리 + 접근 허용 · 가장자리 간격) 안의 대상만 본다. /// 등급 0 = 사선이 빔(화살이 처음 맞힐 유닛 = 그 대상) + (책사) 맞을 자리 광역에 자기 편 없음 · 1 = 사선만 빔(책사) · 2 = 막힘 → 등급 낮은 순 · 같으면 가까운 순. /// 범위 안에 아무도 없으면 가장 가까운 대상(접근). /// static Unit PickRangedTarget(Unit e, IReadOnlyList targets, Unit nearest, BalanceConfig cfg) { Unit pick = null; int pickTier = int.MaxValue; float pickD = float.MaxValue; float reach = SkillMods.Range(e) + e.ArcherMoveAllow; for (int i = 0; i < targets.Count; i++) { var t = targets[i]; if (!t.IsAlive) continue; float d = Vector2.Distance(e.Position, t.Position); if (d - e.Radius * Combat.CastRadiusScale - t.Radius > reach || d < 0.001f) continue; float travel; var first = Combat.FirstUnitOnArrow(e, e.Position, (t.Position - e.Position) / d, d, cfg.arrowRadius, out travel); bool clear = first == t; bool safe = true; if (clear && e.Type == UnitType.Strategist) { foreach (var s in Combat.UnitsInCircle(t.Position, cfg.strategistSplashRadius, t, e)) { if (s.Faction == e.Faction) safe = false; } } int tier = clear ? (safe ? 0 : 1) : 2; if (tier < pickTier || (tier == pickTier && d < pickD)) { pick = t; pickTier = tier; pickD = d; } } return pick ?? nearest; } } }