ThreeKingdomsSlingshot/Assets/Scripts/Runtime/Gimmicks/GimmickShape.cs

193 lines
7.3 KiB
C#
Raw Permalink Normal View History

지형 기믹 레이어(PD #1097 세션 D) + 병종 규칙 개정 2건 · 예측 시뮬레이션 적 AI(PD #1098) - 데이터: Assets/Data/Gimmicks/{gimmicks,stage_gimmicks,tutorial_tips}.csv + Assets/Data/AI/ai_weights.csv(기획 tables/ 사본) → 메뉴 TKS/Import Gimmicks(검증 · 오류 시 자산 불변) → Resources/Gimmicks/GimmickDb(16)·StageGimmickTable(16 + 팁 3) · Resources/AI/AiWeights(32) - 레이어: GimmickLayer(전투 시작 = 스테이지 배치 · 배경 위 유닛 아래 · 원/사각/다각 · 색 도형 + 글자 · 내구 핍 · 강 콜라이더 = 다리 구간 제외 · 겹친 유닛 보정) · 장애물 = 병종별 내구 피해·튕김·창 목책 밀기·바위 굴림·기병 정지 · 투사체 pass/block/destroy · 폭약 광역(아군 포함)+밀기+불 · 책사 광역 덤불 제거 · 강 낙하 - 지대: 경로 스윕(독 1→2→3 % · 늪 ×2 · 불 10 % · 빙판 미끄러짐 · 고지 · 안개 · 군기 · 함정) · 양 진영 턴 시작 훅 - 규칙 개정: 보병 튕김 중 다른 적과 연쇄 1회 · 창병 반작용(spearRecoilRatio · 밀며 전진 폐기) · 기존 테스트 기대값 갱신 - 적 AI: Runtime/AI(Sim 가상 실행 · TacticalAI 후보·점수·난이도·턴 운영 · 1부 보스 액티브) · 조준선 = 가상 실행 예상 궤적·기믹 반응·지대 띠 · TKS/Gimmick Painter · 캡처 GimmickImporter.Capture - 검증: 컴파일 0 · 플레이모드 113/113(신규 26) · console error 0 · 적 10기 턴 결정 62 ms Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:20:07 +00:00
using UnityEngine;
namespace TKS
{
/// <summary>
/// 기믹 모양(circle · rect(회전) · poly) 기하 — 지대 겹침(경로 스윕) · 거리 · 진입/이탈 거리. 장애물 콜라이더도 이 모양으로 만든다.
/// 경로 스윕은 0.05 간격 표본(전장 20×32 · 슬링 최대 6.5 → 한 행동 130 표본 × 기믹 수 · 충분히 가볍다).
/// </summary>
public struct GimmickShape
{
public const float SweepStep = 0.05f;
public GimmickShapeType Type;
public Vector2 Center;
/// <summary>circle 반지름.</summary>
public float R;
/// <summary>rect 가로·세로.</summary>
public Vector2 Size;
/// <summary>rect 회전(도 · 반시계).</summary>
public float Rot;
/// <summary>poly 꼭짓점(월드 · Center 기준 상대 좌표가 아니다).</summary>
public Vector2[] Points;
public static GimmickShape From(GimmickPlacement p)
{
var s = new GimmickShape { Type = p.shape, Center = new Vector2(p.cx, p.cy), R = p.wOrR, Size = new Vector2(p.wOrR, p.h), Rot = p.rot };
if (p.shape == GimmickShapeType.Poly) s.Points = p.points != null ? (Vector2[])p.points.Clone() : new Vector2[0];
return s;
}
public static GimmickShape Circle(Vector2 c, float r)
{
return new GimmickShape { Type = GimmickShapeType.Circle, Center = c, R = r };
}
public static GimmickShape Box(Vector2 c, Vector2 size, float rot = 0f)
{
return new GimmickShape { Type = GimmickShapeType.Rect, Center = c, Size = size, Rot = rot };
}
public GimmickShape Translated(Vector2 d)
{
var s = this;
s.Center += d;
if (Type == GimmickShapeType.Poly && Points != null)
{
s.Points = new Vector2[Points.Length];
for (int i = 0; i < Points.Length; i++) s.Points[i] = Points[i] + d;
}
return s;
}
/// <summary>가장 바깥 점까지의 거리(대략 검사용).</summary>
public float BoundingRadius
{
get
{
switch (Type)
{
case GimmickShapeType.Circle: return R;
case GimmickShapeType.Rect: return Size.magnitude * 0.5f;
}
float m = 0f;
if (Points != null) foreach (var p in Points) m = Mathf.Max(m, Vector2.Distance(p, Center));
return m;
}
}
public Vector2 ToLocal(Vector2 q)
{
Vector2 d = q - Center;
float a = -Rot * Mathf.Deg2Rad;
float c = Mathf.Cos(a), s = Mathf.Sin(a);
return new Vector2(d.x * c - d.y * s, d.x * s + d.y * c);
}
public Vector2 LocalAxisX
{
get
{
float a = Rot * Mathf.Deg2Rad;
return new Vector2(Mathf.Cos(a), Mathf.Sin(a));
}
}
/// <summary>점 q 까지 부호 거리(안 = 음수 · 경계 = 0).</summary>
public float SignedDistance(Vector2 q)
{
switch (Type)
{
case GimmickShapeType.Circle:
return Vector2.Distance(q, Center) - R;
case GimmickShapeType.Rect:
{
Vector2 l = ToLocal(q);
Vector2 d = new Vector2(Mathf.Abs(l.x) - Size.x * 0.5f, Mathf.Abs(l.y) - Size.y * 0.5f);
Vector2 o = new Vector2(Mathf.Max(d.x, 0f), Mathf.Max(d.y, 0f));
return o.magnitude + Mathf.Min(Mathf.Max(d.x, d.y), 0f);
}
}
if (Points == null || Points.Length < 3) return float.MaxValue;
float min = float.MaxValue;
bool inside = false;
for (int i = 0, j = Points.Length - 1; i < Points.Length; j = i++)
{
Vector2 a = Points[i], b = Points[j];
min = Mathf.Min(min, Combat.SegmentPointDistance(a, b, q));
if ((a.y > q.y) != (b.y > q.y) && q.x < (b.x - a.x) * (q.y - a.y) / (b.y - a.y) + a.x) inside = !inside;
}
return inside ? -min : min;
}
public bool Contains(Vector2 q)
{
return SignedDistance(q) <= 0f;
}
/// <summary>선분 ab 가 모양과 겹치는가(중심 경로 · 여유 tol).</summary>
public bool SegmentTouches(Vector2 a, Vector2 b, float tol = 0f)
{
float len = Vector2.Distance(a, b);
if (SegmentPointDistanceToCenter(a, b) > BoundingRadius + tol + 0.01f) return false;
int n = Mathf.Max(1, Mathf.CeilToInt(len / SweepStep));
for (int i = 0; i <= n; i++)
{
if (SignedDistance(Vector2.Lerp(a, b, (float)i / n)) <= tol) return true;
}
return false;
}
/// <summary>선분 ab 중 모양 안에 든 길이.</summary>
public float InsideLength(Vector2 a, Vector2 b)
{
float len = Vector2.Distance(a, b);
if (len < 1e-4f || SegmentPointDistanceToCenter(a, b) > BoundingRadius + 0.01f) return 0f;
int n = Mathf.Max(1, Mathf.CeilToInt(len / SweepStep));
float step = len / n;
float inside = 0f;
for (int i = 0; i < n; i++)
{
if (Contains(Vector2.Lerp(a, b, (i + 0.5f) / n))) inside += step;
}
return inside;
}
/// <summary>origin 에서 dir 로 dist 안에 중심이 처음 모양 안에 드는 거리(출발점이 이미 안이면 0 · 없으면 −1).</summary>
public float EnterDistance(Vector2 origin, Vector2 dir, float dist)
{
if (Contains(origin)) return 0f;
if (SegmentPointDistanceToCenter(origin, origin + dir * dist) > BoundingRadius + 0.01f) return -1f;
int n = Mathf.Max(1, Mathf.CeilToInt(dist / SweepStep));
float prev = 0f;
for (int i = 1; i <= n; i++)
{
float t = dist * i / n;
if (!Contains(origin + dir * t))
{
prev = t;
continue;
}
// 이분법으로 경계를 좁힌다
float lo = prev, hi = t;
for (int k = 0; k < 8; k++)
{
float m = (lo + hi) * 0.5f;
if (Contains(origin + dir * m)) hi = m;
else lo = m;
}
return hi;
}
return -1f;
}
/// <summary>origin(모양 안)에서 dir 로 나가는 거리(max 까지 · 출발점이 밖이면 0).</summary>
public float ExitDistance(Vector2 origin, Vector2 dir, float max)
{
if (!Contains(origin)) return 0f;
int n = Mathf.Max(1, Mathf.CeilToInt(max / SweepStep));
for (int i = 1; i <= n; i++)
{
float t = max * i / n;
if (!Contains(origin + dir * t)) return t;
}
return max;
}
float SegmentPointDistanceToCenter(Vector2 a, Vector2 b)
{
return Combat.SegmentPointDistance(a, b, Center);
}
}
}