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); } } }