using System.Collections.Generic;
using UnityEngine;
namespace TKS
{
///
/// 조준 미리보기(PD #1097 §6 + #1098 §3): 이 슬링샷을 가상 실행(Sim · 연출 없음 · 숨은 함정 모름)해 그 결과를 그린다 —
/// 행동 유닛 실제 경로(튕김·연쇄·창병 반작용·빙판 미끄러짐 = 점선) · 밀릴 유닛 궤적(상대 = 주황 · 같은 편 = 하늘 점선) ·
/// 장애물에 닿는 지점 = 붉게 끊김 + 결과 글자(내구 −n · 파괴 · 굴림 · 밀림 · 폭발 · 튕김) · 지나는 지대 = 색 띠(늪 갈색 · 독 보라 · 불 주황 · 빙판 하늘 · 고지 황토 · 안개 흰색).
///
public class GimmickPreview
{
static readonly Color MoverColor = new Color(1f, 0.95f, 0.5f, 0.95f);
static readonly Color FoeTrackColor = new Color(1f, 0.6f, 0.15f, 0.9f);
static readonly Color AllyTrackColor = new Color(0.45f, 0.8f, 1f, 0.9f);
static readonly Color CutColor = new Color(1f, 0.15f, 0.15f, 0.9f);
static readonly Color IconColor = new Color(1f, 0.85f, 0.35f);
readonly GimmickLayer layer;
readonly Transform root;
readonly List dashes = new List();
readonly List bands = new List();
readonly List icons = new List();
LineRenderer cut;
static Material dashMaterial;
static Material solidMaterial;
int usedDash, usedBand, usedIcon;
/// 마지막 미리보기 결과(테스트 확인용).
public readonly List LastIcons = new List();
public readonly List LastBandColors = new List();
public int LastTracks;
public bool LastCut;
public Vector2 LastMoverEnd;
public GimmickPreview(GimmickLayer l)
{
layer = l;
var go = new GameObject("AimPreview");
go.transform.SetParent(l.transform, false);
root = go.transform;
}
public static Color BandColor(GimmickZone z)
{
switch (z)
{
case GimmickZone.Slow: return new Color(0.55f, 0.38f, 0.15f, 0.75f);
case GimmickZone.Poison: return new Color(0.7f, 0.35f, 0.95f, 0.75f);
case GimmickZone.Burn: return new Color(1f, 0.5f, 0.1f, 0.75f);
case GimmickZone.Slide: return new Color(0.55f, 0.9f, 1f, 0.75f);
case GimmickZone.High: return new Color(0.85f, 0.7f, 0.35f, 0.7f);
case GimmickZone.Fog: return new Color(0.95f, 0.95f, 1f, 0.6f);
case GimmickZone.River: return new Color(0.25f, 0.5f, 0.95f, 0.75f);
}
return new Color(1f, 1f, 1f, 0f);
}
public bool Visible
{
get { return usedDash + usedBand + usedIcon > 0 || (cut != null && cut.enabled); }
}
public void Hide()
{
usedDash = usedBand = usedIcon = 0;
Flush();
if (cut != null) cut.enabled = false;
}
/// 조준 중 1회(드래그 프레임마다). shot = 궁·책 사격 계획(있으면 막힘·폭발 글자).
public void Show(Unit u, Vector2 target, bool moveOnly, ActionResolver.ShotPlan shot)
{
usedDash = usedBand = usedIcon = 0;
LastIcons.Clear();
LastBandColors.Clear();
LastTracks = 0;
LastCut = false;
if (cut != null) cut.enabled = false;
var bm = layer.Battle;
if (u == null || bm == null || !u.IsAlive)
{
Flush();
return;
}
// 가상 실행(연출 없음 · 숨은 함정 모름 · 상태는 되돌린다)
var snap = Sim.Take(bm);
var gBefore = new List>();
var gPos = new List();
foreach (var g in layer.Items)
{
gBefore.Add(new KeyValuePair(g, g.Hp * 4 + (g.Broken ? 1 : 0) + (g.Removed ? 2 : 0)));
gPos.Add(g.Shape.Center);
}
var o = new ActionOutcome();
var events = new List();
var moverPath = new List();
var tracks = new List>();
Sim.Active = true;
GimmickLayer.PreviewMode = true;
try
{
var resolver = new ActionResolver(bm.Config, bm, new System.Random(7));
Sim.Run(resolver.Resolve(u, target, moveOnly, o));
moverPath.AddRange(o.Path);
foreach (var d in o.Displaced) tracks.Add(new KeyValuePair(d.Unit, d));
for (int i = 0; i < gBefore.Count && i < layer.Items.Count; i++)
{
var g = gBefore[i].Key;
int before = gBefore[i].Value;
int beforeHp = before / 4;
if (g.Broken && (before & 1) == 0) events.Add(g.Def.projectile == GimmickProjectile.Destroy ? g.Def.nameKo + " 폭발" : g.Def.nameKo + " 파괴");
else if (g.Hp < beforeHp) events.Add(g.Def.nameKo + " −" + (beforeHp - g.Hp));
if (g.Removed && (before & 2) == 0 && g.Zone != GimmickZone.Trap) events.Add(g.Def.nameKo + " 제거");
if ((g.Shape.Center - gPos[i]).sqrMagnitude > 1e-4f) events.Add(g.Def.nameKo + (g.Rolls ? " 굴림" : " 밀림"));
}
if (o.Chains > 0) events.Add("연쇄 " + o.Chains);
foreach (var n in o.Notes)
{
if (n.Contains("미끄러짐")) events.Add("미끄러짐");
if (n.Contains("강에 빠짐")) events.Add("강에 빠짐");
if (n.Contains("안개 걷힘")) events.Add("안개 걷힘");
int k = n.IndexOf("자기 −");
if (k >= 0) events.Add(n.Substring(k)); // 튕김 자기 피해(bounce_pct · cav_self_pct)
}
}
finally
{
Sim.Active = false;
GimmickLayer.PreviewMode = false;
Sim.Restore(bm, snap);
}
// 행동 유닛 = 첫 접촉 뒤 궤적(튕김·연쇄·반작용·미끄러짐)만 점선(첫 구간은 조준선) · 첫 구간이 장애물에서 끝났으면 그 뒤(조준점까지)를 붉게 끊는다
if (moverPath.Count >= 2)
{
LastMoverEnd = moverPath[moverPath.Count - 1];
if (moverPath.Count > 2)
{
Dash(moverPath.GetRange(1, moverPath.Count - 1), MoverColor);
LastTracks++;
}
Vector2 aimEnd = bm.Resolver.ReachPoint(u, target);
Vector2 firstEnd = moverPath[1];
if (!UnitTypeInfo.IsRanged(u.Type) && Vector2.Distance(firstEnd, aimEnd) > 0.15f && BlockNear(firstEnd, u.Radius))
{
Cut(firstEnd, aimEnd);
if (o.Path.Count > 2 && !events.Contains("미끄러짐")) events.Add("튕김");
}
Bands(moverPath, u.Faction == Faction.Player);
}
foreach (var kv in tracks)
{
if (kv.Key == null || Vector2.Distance(kv.Value.From, kv.Value.To) < 0.05f) continue;
Dash(new List { kv.Value.From, kv.Value.To }, kv.Key.Faction == u.Faction ? AllyTrackColor : FoeTrackColor);
LastTracks++;
}
if (shot != null && shot.HitGimmick != null && shot.Hit == null)
{
string t = shot.HitGimmick.Def.projectile == GimmickProjectile.Destroy ? shot.HitGimmick.Def.nameKo + " 폭발" : shot.HitGimmick.Def.nameKo + "에 막힘";
if (!events.Contains(t)) events.Add(t);
}
if (events.Count > 0)
{
Vector2 at = shot != null && shot.Fires ? shot.To : (moverPath.Count >= 2 ? moverPath[1] : u.Position);
Icon(at, string.Join(" · ", events.ToArray()));
}
Flush();
}
/// 그 지점에서 장애물 콜라이더(강 = 다리 구간을 뺀 조각)에 닿아 있는가.
static bool BlockNear(Vector2 p, float r)
{
Physics2D.SyncTransforms();
foreach (var c in Physics2D.OverlapCircleAll(p, r * Combat.CastRadiusScale + 0.08f))
{
if (GimmickLayer.BlocksMove(c)) return true;
}
return false;
}
void Bands(List path, bool player)
{
foreach (var g in layer.Items)
{
if (!g.ZoneActive || g.Zone == GimmickZone.Trap || g.Zone == GimmickZone.Capture || g.Zone == GimmickZone.None) continue;
Color c = BandColor(g.Zone);
for (int i = 0; i + 1 < path.Count; i++)
{
Vector2 a = path[i], b = path[i + 1];
float len = Vector2.Distance(a, b);
if (len < 0.05f) continue;
int n = Mathf.Max(1, Mathf.CeilToInt(len / GimmickShape.SweepStep));
int start = -1;
for (int k = 0; k <= n; k++)
{
bool inside = Inside(g, Vector2.Lerp(a, b, (float)k / n));
if (inside && start < 0) start = k;
if ((!inside || k == n) && start >= 0)
{
int end = inside ? k : k - 1;
if (end > start)
{
Band(Vector2.Lerp(a, b, (float)start / n), Vector2.Lerp(a, b, (float)end / n), c);
LastBandColors.Add(c);
}
start = -1;
}
}
}
}
}
/// 띠를 그릴 자리: 강 = 다리 구간 제외 · 안개 = 걷힌 자리 제외.
bool Inside(GimmickInstance g, Vector2 p)
{
if (!g.Shape.Contains(p)) return false;
if (g.Zone == GimmickZone.River) return layer.RiverAt(p) != null;
if (g.Zone == GimmickZone.Fog) return layer.FogAt(p) != null;
return true;
}
// ---------- 그리기(풀) ----------
static Material DashMaterial
{
get
{
if (dashMaterial == null)
{
var tex = new Texture2D(8, 2, TextureFormat.RGBA32, false);
var px = new Color[16];
for (int y = 0; y < 2; y++)
for (int x = 0; x < 8; x++)
px[y * 8 + x] = x < 5 ? Color.white : new Color(1f, 1f, 1f, 0f);
tex.SetPixels(px);
tex.wrapMode = TextureWrapMode.Repeat;
tex.filterMode = FilterMode.Point;
tex.Apply();
tex.hideFlags = HideFlags.DontSave;
dashMaterial = new Material(Shader.Find("Sprites/Default")) { mainTexture = tex };
dashMaterial.mainTextureScale = new Vector2(4f, 1f);
dashMaterial.hideFlags = HideFlags.DontSave;
}
return dashMaterial;
}
}
static Material SolidMaterial
{
get
{
if (solidMaterial == null) solidMaterial = new Material(Shader.Find("Sprites/Default")) { hideFlags = HideFlags.DontSave };
return solidMaterial;
}
}
LineRenderer NewLine(string name, Material m, float width, int order, bool tile)
{
var go = new GameObject(name);
go.transform.SetParent(root, false);
var lr = go.AddComponent();
lr.useWorldSpace = true;
lr.sharedMaterial = m;
lr.startWidth = lr.endWidth = width;
lr.sortingOrder = order;
lr.numCapVertices = tile ? 0 : 3;
lr.textureMode = tile ? LineTextureMode.Tile : LineTextureMode.Stretch;
return lr;
}
void Dash(List pts, Color c)
{
if (usedDash >= dashes.Count) dashes.Add(NewLine("Track", DashMaterial, 0.06f, 9, true));
var lr = dashes[usedDash++];
lr.positionCount = pts.Count;
for (int i = 0; i < pts.Count; i++) lr.SetPosition(i, new Vector3(pts[i].x, pts[i].y, 0f));
lr.startColor = lr.endColor = c;
lr.enabled = true;
}
void Band(Vector2 a, Vector2 b, Color c)
{
if (usedBand >= bands.Count) bands.Add(NewLine("ZoneBand", SolidMaterial, 0.22f, 8, false));
var lr = bands[usedBand++];
lr.positionCount = 2;
lr.SetPosition(0, new Vector3(a.x, a.y, 0f));
lr.SetPosition(1, new Vector3(b.x, b.y, 0f));
lr.startColor = lr.endColor = c;
lr.enabled = true;
}
void Cut(Vector2 a, Vector2 b)
{
if (cut == null) cut = NewLine("BlockedCut", SolidMaterial, 0.13f, 11, false); // 조준선(10) 위
cut.positionCount = 2;
cut.SetPosition(0, new Vector3(a.x, a.y, 0f));
cut.SetPosition(1, new Vector3(b.x, b.y, 0f));
cut.startColor = cut.endColor = CutColor;
cut.enabled = true;
LastCut = true;
}
void Icon(Vector2 at, string text)
{
if (usedIcon >= icons.Count)
{
var tm = TextUtil.CreateWorldText(root, "", 0.3f, IconColor, 12, TextAnchor.MiddleLeft);
tm.gameObject.name = "PreviewIcon";
icons.Add(tm);
}
var t = icons[usedIcon++];
t.text = text;
t.transform.position = new Vector3(at.x + 0.4f, at.y - 0.3f, 0f); // 접점 오른쪽 아래(기믹 이름·조준선과 겹치지 않게)
t.gameObject.SetActive(true);
LastIcons.Add(text);
}
void Flush()
{
for (int i = usedDash; i < dashes.Count; i++) dashes[i].enabled = false;
for (int i = usedBand; i < bands.Count; i++) bands[i].enabled = false;
for (int i = usedIcon; i < icons.Count; i++) icons[i].gameObject.SetActive(false);
}
}
}