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

328 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using UnityEngine;
namespace TKS
{
/// <summary>
/// 조준 미리보기(PD #1097 §6 + #1098 §3): 이 슬링샷을 가상 실행(Sim · 연출 없음 · 숨은 함정 모름)해 그 결과를 그린다 —
/// 행동 유닛 실제 경로(튕김·연쇄·창병 반작용·빙판 미끄러짐 = 점선) · 밀릴 유닛 궤적(상대 = 주황 · 같은 편 = 하늘 점선) ·
/// 장애물에 닿는 지점 = 붉게 끊김 + 결과 글자(내구 −n · 파괴 · 굴림 · 밀림 · 폭발 · 튕김) · 지나는 지대 = 색 띠(늪 갈색 · 독 보라 · 불 주황 · 빙판 하늘 · 고지 황토 · 안개 흰색).
/// </summary>
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<LineRenderer> dashes = new List<LineRenderer>();
readonly List<LineRenderer> bands = new List<LineRenderer>();
readonly List<TextMesh> icons = new List<TextMesh>();
LineRenderer cut;
static Material dashMaterial;
static Material solidMaterial;
int usedDash, usedBand, usedIcon;
/// <summary>마지막 미리보기 결과(테스트 확인용).</summary>
public readonly List<string> LastIcons = new List<string>();
public readonly List<Color> LastBandColors = new List<Color>();
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;
}
/// <summary>조준 중 1회(드래그 프레임마다). shot = 궁·책 사격 계획(있으면 막힘·폭발 글자).</summary>
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<KeyValuePair<GimmickInstance, int>>();
var gPos = new List<Vector2>();
foreach (var g in layer.Items)
{
gBefore.Add(new KeyValuePair<GimmickInstance, int>(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<string>();
var moverPath = new List<Vector2>();
var tracks = new List<KeyValuePair<Unit, Displacement>>();
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<Unit, Displacement>(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<Vector2> { 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();
}
/// <summary>그 지점에서 장애물 콜라이더(강 = 다리 구간을 뺀 조각)에 닿아 있는가.</summary>
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<Vector2> 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;
}
}
}
}
}
/// <summary>띠를 그릴 자리: 강 = 다리 구간 제외 · 안개 = 걷힌 자리 제외.</summary>
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<LineRenderer>();
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<Vector2> 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);
}
}
}