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

328 lines
14 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 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);
지형 기믹 레이어(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
}
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("안개 걷힘");
지형 기믹 레이어(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
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));
지형 기믹 레이어(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
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;
}
지형 기믹 레이어(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
// ---------- 그리기(풀) ----------
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);
}
}
}