2026-11-03 19:10:10 +00:00
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
namespace TKS
|
|
|
|
|
{
|
2026-11-05 04:06:32 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// 적군 슬링샷 결정: 교전 범위 안의 가장 가까운 아군을 향해 당기고, 정확도에 따라 흔들린다. 범위 밖이면 대기(null).
|
|
|
|
|
/// 궁병·책사(PD 2026-11-05 #1087 ⑤⑥ · cfg 를 넘기면): 공격 가능 범위 안에서 사선이 빈 대상 우선 · 책사는 맞을 자리 광역에 자기 편이 없는 대상을 그보다 먼저 · 없으면 가장 가까운 대상(그냥 쏜다).
|
|
|
|
|
/// </summary>
|
2026-11-03 19:10:10 +00:00
|
|
|
public static class EnemyAI
|
|
|
|
|
{
|
2026-11-05 04:06:32 +00:00
|
|
|
public static Vector2? Decide(Unit e, IReadOnlyList<Unit> targets, float accuracy, float aggroRange, System.Random rng, Board board, BalanceConfig cfg = null)
|
2026-11-03 19:10:10 +00:00
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-11-03 19:29:16 +00:00
|
|
|
if (nearest == null) return null;
|
|
|
|
|
if (best > aggroRange) return null; // 넓은 전장: 교전 범위 밖이면 자리 지킴
|
2026-11-03 19:10:10 +00:00
|
|
|
|
2026-11-05 04:06:32 +00:00
|
|
|
if (cfg != null && UnitTypeInfo.IsRanged(e.Type)) nearest = PickRangedTarget(e, targets, nearest, cfg);
|
|
|
|
|
|
2026-11-03 19:10:10 +00:00
|
|
|
Vector2 toT = nearest.Position - e.Position;
|
|
|
|
|
float dist = toT.magnitude;
|
|
|
|
|
Vector2 dir = dist > 0.001f ? toT / dist : Vector2.up;
|
2026-11-05 14:31:27 +00:00
|
|
|
float sling = SkillMods.SlingMax(e); // 적 무장 패시브(적토마 등 · PD #1093 B) 반영
|
2026-11-03 19:10:10 +00:00
|
|
|
|
|
|
|
|
Vector2 target;
|
2026-11-05 14:31:27 +00:00
|
|
|
if (dist <= sling)
|
2026-11-03 19:10:10 +00:00
|
|
|
{
|
|
|
|
|
target = nearest.Position; // 최종 목표점 명중 시도
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-11-05 14:31:27 +00:00
|
|
|
target = e.Position + dir * sling; // 접근(궁병은 사거리 안이면 이동 후 사격)
|
2026-11-03 19:10:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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());
|
2026-11-05 14:31:27 +00:00
|
|
|
target = e.Position + rd * Mathf.Min(len, sling);
|
2026-11-03 19:10:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Vector2 v = target - e.Position;
|
2026-11-05 14:31:27 +00:00
|
|
|
if (v.magnitude > sling) v = v.normalized * sling;
|
2026-11-03 19:10:10 +00:00
|
|
|
return board.ClampInside(e.Position + v, e.Radius);
|
|
|
|
|
}
|
2026-11-05 04:06:32 +00:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 원거리 대상 고르기: 공격 가능 범위(사거리 + 접근 허용 · 가장자리 간격) 안의 대상만 본다.
|
|
|
|
|
/// 등급 0 = 사선이 빔(화살이 처음 맞힐 유닛 = 그 대상) + (책사) 맞을 자리 광역에 자기 편 없음 · 1 = 사선만 빔(책사) · 2 = 막힘 → 등급 낮은 순 · 같으면 가까운 순.
|
|
|
|
|
/// 범위 안에 아무도 없으면 가장 가까운 대상(접근).
|
|
|
|
|
/// </summary>
|
|
|
|
|
static Unit PickRangedTarget(Unit e, IReadOnlyList<Unit> targets, Unit nearest, BalanceConfig cfg)
|
|
|
|
|
{
|
|
|
|
|
Unit pick = null;
|
|
|
|
|
int pickTier = int.MaxValue;
|
|
|
|
|
float pickD = float.MaxValue;
|
2026-11-05 14:31:27 +00:00
|
|
|
float reach = SkillMods.Range(e) + e.ArcherMoveAllow;
|
2026-11-05 04:06:32 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2026-11-03 19:10:10 +00:00
|
|
|
}
|
|
|
|
|
}
|