ThreeKingdomsSlingshot/Assets/Scripts/Editor/GimmickPainter.cs

295 lines
13 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 System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
namespace TKS.EditorTools
{
/// <summary>
/// 지형 기믹 배치 도구(PD #1097 §1 「어떤 맵이든 올려서 배치」) — 메뉴 TKS/Gimmick Painter.
/// 스테이지 고르기 → 불러오기(stage_gimmicks.csv 의 그 스테이지 행) → 기믹·모양 고르고 「그리기」 → 씬 뷰에서 드래그(원 = 중심→반지름 · 사각 = 모서리→모서리) →
/// 저장(그 스테이지 행만 바꾸고 다른 스테이지 행은 그대로 · utf-8-sig) → 자동 임포트(Play 중이면 레이어 다시 만듦). 표를 바꿨으면 기획 tables/ 에도 같은 파일을 둔다.
/// </summary>
public class GimmickPainter : EditorWindow
{
const string Header = "stage,gimmick_id,shape,cx,cy,w_or_r,h,rot,points,hp_override,params";
int stageIdx;
int gimmickIdx;
GimmickShapeType shape = GimmickShapeType.Rect;
float rot;
string param = "";
bool painting;
int selected = -1;
Vector2 scroll;
readonly List<GimmickPlacement> rows = new List<GimmickPlacement>();
string loadedStage = "";
bool dragging;
Vector2 dragStart, dragNow;
[MenuItem("TKS/Gimmick Painter (기믹 배치)")]
public static void Open()
{
GetWindow<GimmickPainter>("기믹 배치");
}
void OnEnable()
{
SceneView.duringSceneGui += OnScene;
}
void OnDisable()
{
SceneView.duringSceneGui -= OnScene;
}
static BalanceConfig Cfg
{
get { return AssetDatabase.LoadAssetAtPath<BalanceConfig>(ProjectSetup.ConfigPath); }
}
string CurrentStage
{
get
{
var cfg = Cfg;
if (cfg == null || cfg.stages.Length == 0) return "";
var s = cfg.stages[Mathf.Clamp(stageIdx, 0, cfg.stages.Length - 1)];
return s.chapter + "-" + s.stage;
}
}
void OnGUI()
{
var cfg = Cfg;
var db = GimmickTables.Db;
if (cfg == null || db == null)
{
EditorGUILayout.HelpBox("BalanceConfig 또는 GimmickDb 가 없습니다 — TKS/Import Stages · TKS/Import Gimmicks 먼저.", MessageType.Warning);
return;
}
var stageNames = new string[cfg.stages.Length];
for (int i = 0; i < stageNames.Length; i++) stageNames[i] = cfg.stages[i].chapter + "-" + cfg.stages[i].stage + " " + cfg.stages[i].title;
if (Application.isPlaying && BattleManager.Instance != null && GUILayout.Button("지금 전투 스테이지로"))
{
var st = BattleManager.Instance.Stage;
for (int i = 0; i < cfg.stages.Length; i++)
if (cfg.stages[i].chapter == st.chapter && cfg.stages[i].stage == st.stage) stageIdx = i;
}
stageIdx = EditorGUILayout.Popup("스테이지", Mathf.Clamp(stageIdx, 0, stageNames.Length - 1), stageNames);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("불러오기")) LoadRows();
GUI.enabled = loadedStage == CurrentStage && loadedStage.Length > 0;
if (GUILayout.Button("저장 + 임포트")) SaveRows();
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
if (loadedStage != CurrentStage) EditorGUILayout.HelpBox("이 스테이지를 불러온 뒤 그리세요(불러온 = " + (loadedStage.Length > 0 ? loadedStage : "없음") + ").", MessageType.Info);
var ids = new string[db.defs.Count];
for (int i = 0; i < ids.Length; i++) ids[i] = db.defs[i].id + " · " + db.defs[i].nameKo;
gimmickIdx = EditorGUILayout.Popup("기믹", Mathf.Clamp(gimmickIdx, 0, ids.Length - 1), ids);
shape = (GimmickShapeType)EditorGUILayout.Popup("모양", shape == GimmickShapeType.Circle ? 0 : 1, new[] { "circle", "rect" }) == 0 ? GimmickShapeType.Circle : GimmickShapeType.Rect;
if (shape == GimmickShapeType.Rect) rot = EditorGUILayout.FloatField("회전(도)", rot);
param = EditorGUILayout.TextField("params", param);
painting = GUILayout.Toggle(painting, painting ? "그리기 중 — 씬 뷰에서 드래그 (다시 누르면 끔)" : "그리기", "Button");
scroll = EditorGUILayout.BeginScrollView(scroll);
for (int i = 0; i < rows.Count; i++)
{
var r = rows[i];
EditorGUILayout.BeginHorizontal();
if (GUILayout.Toggle(selected == i, "", GUILayout.Width(18))) selected = i;
EditorGUILayout.LabelField(r.gimmickId + " " + r.shape + " (" + GimmickTables.F(r.cx) + ", " + GimmickTables.F(r.cy) + ") " + GimmickTables.F(r.wOrR) + (r.shape == GimmickShapeType.Rect ? "×" + GimmickTables.F(r.h) : "") + (r.paramsRaw.Length > 0 ? " · " + r.paramsRaw : ""));
if (GUILayout.Button("삭제", GUILayout.Width(44)))
{
rows.RemoveAt(i);
selected = -1;
SceneView.RepaintAll();
break;
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
}
void LoadRows()
{
rows.Clear();
selected = -1;
var texts = GimmickImporter.ReadFiles();
var r = GimmickTables.Parse(texts);
string st = CurrentStage;
foreach (var p in r.rows)
{
if (p.stage == st) rows.Add(p);
}
loadedStage = st;
if (r.errors.Count > 0) Debug.LogWarning("[TKS] 기믹 배치 표 경고 " + r.errors.Count + "건(첫 줄: " + r.errors[0] + ")");
SceneView.RepaintAll();
}
void SaveRows()
{
string path = Path.Combine(Application.dataPath, GimmickTables.Folder, GimmickTables.Files[1]);
var lines = File.Exists(path) ? File.ReadAllLines(path, Encoding.UTF8) : new string[0];
var outLines = new List<string> { Header };
bool emitted = false;
for (int i = 1; i < lines.Length; i++)
{
var line = lines[i].TrimStart('');
if (line.Trim().Length == 0) continue;
string first = line.Split(',')[0].Trim();
if (first == loadedStage)
{
if (!emitted) foreach (var r in rows) outLines.Add(Format(r));
emitted = true;
continue;
}
outLines.Add(line);
}
if (!emitted) foreach (var r in rows) outLines.Add(Format(r));
File.WriteAllText(path, string.Join("\n", outLines.ToArray()) + "\n", new UTF8Encoding(true));
AssetDatabase.Refresh();
Debug.Log("[TKS] 기믹 배치 저장 · " + loadedStage + " " + rows.Count + "행 · " + path);
if (GimmickImporter.ImportAll() && Application.isPlaying && BattleManager.Instance != null && GimmickLayer.Current != null) GimmickLayer.Current.Build(BattleManager.Instance.Stage);
}
static string Format(GimmickPlacement r)
{
string shp = r.shape == GimmickShapeType.Circle ? "circle" : r.shape == GimmickShapeType.Rect ? "rect" : "poly";
return string.Join(",", new[]
{
r.stage, r.gimmickId, shp,
r.shape == GimmickShapeType.Poly ? "" : GimmickTables.F(r.cx),
r.shape == GimmickShapeType.Poly ? "" : GimmickTables.F(r.cy),
r.shape == GimmickShapeType.Poly ? "" : GimmickTables.F(r.wOrR),
r.shape == GimmickShapeType.Rect ? GimmickTables.F(r.h) : "",
GimmickTables.F(r.rot),
r.shape == GimmickShapeType.Poly ? GimmickTables.FormatPoints(r.points) : "",
r.hpOverride > 0 ? r.hpOverride.ToString() : "",
r.paramsRaw ?? ""
});
}
static Vector2 MouseWorld(Event e)
{
var ray = HandleUtility.GUIPointToWorldRay(e.mousePosition);
if (Mathf.Abs(ray.direction.z) < 1e-5f) return ray.origin;
float t = -ray.origin.z / ray.direction.z;
var p = ray.origin + ray.direction * t;
return new Vector2(Mathf.Round(p.x * 10f) / 10f, Mathf.Round(p.y * 10f) / 10f);
}
void OnScene(SceneView view)
{
var cfg = Cfg;
if (cfg != null)
{
Vector2 b = cfg.boardSize;
Handles.color = new Color(1f, 0.85f, 0.4f, 0.8f);
Handles.DrawWireCube(Vector3.zero, new Vector3(b.x, b.y, 0f));
}
var db = GimmickTables.Db;
for (int i = 0; i < rows.Count; i++) DrawRow(rows[i], db, i == selected);
if (!painting || db == null || loadedStage != CurrentStage) return;
var e = Event.current;
int id = GUIUtility.GetControlID(FocusType.Passive);
if (e.type == EventType.Layout) HandleUtility.AddDefaultControl(id);
if (e.type == EventType.MouseDown && e.button == 0 && !e.alt)
{
dragging = true;
dragStart = dragNow = MouseWorld(e);
GUIUtility.hotControl = id;
e.Use();
}
else if (e.type == EventType.MouseDrag && dragging)
{
dragNow = MouseWorld(e);
e.Use();
view.Repaint();
}
else if (e.type == EventType.MouseUp && dragging)
{
dragging = false;
GUIUtility.hotControl = 0;
dragNow = MouseWorld(e);
AddRow(db);
e.Use();
Repaint();
}
if (dragging)
{
var tmp = MakeRow(db);
if (tmp != null) DrawRow(tmp, db, true);
}
}
GimmickPlacement MakeRow(GimmickDb db)
{
if (db == null || db.defs.Count == 0) return null;
var def = db.defs[Mathf.Clamp(gimmickIdx, 0, db.defs.Count - 1)];
var p = new GimmickPlacement { stage = loadedStage, gimmickId = def.id, shape = shape, rot = shape == GimmickShapeType.Rect ? rot : 0f, paramsRaw = param ?? "" };
var parts = loadedStage.Split('-');
int.TryParse(parts[0], out p.chapter);
if (parts.Length > 1) int.TryParse(parts[1], out p.stageNo);
if (shape == GimmickShapeType.Circle)
{
p.cx = dragStart.x;
p.cy = dragStart.y;
p.wOrR = Mathf.Max(0.2f, Mathf.Round(Vector2.Distance(dragStart, dragNow) * 10f) / 10f);
}
else
{
Vector2 c = (dragStart + dragNow) * 0.5f;
p.cx = Mathf.Round(c.x * 10f) / 10f;
p.cy = Mathf.Round(c.y * 10f) / 10f;
p.wOrR = Mathf.Max(0.2f, Mathf.Abs(dragNow.x - dragStart.x));
p.h = Mathf.Max(0.2f, Mathf.Abs(dragNow.y - dragStart.y));
}
return p;
}
void AddRow(GimmickDb db)
{
var p = MakeRow(db);
if (p == null) return;
rows.Add(p);
selected = rows.Count - 1;
}
static void DrawRow(GimmickPlacement r, GimmickDb db, bool sel)
{
var def = db != null ? db.Get(r.gimmickId) : null;
Color c = def != null ? GimmickView.ColorOf(def) : Color.magenta;
c.a = Mathf.Max(c.a, 0.35f) * (sel ? 1f : 0.7f);
Handles.color = c;
var s = GimmickShape.From(r);
switch (s.Type)
{
case GimmickShapeType.Circle:
Handles.DrawSolidDisc(new Vector3(s.Center.x, s.Center.y, 0f), Vector3.forward, s.R);
break;
case GimmickShapeType.Rect:
{
Vector2 ax = s.LocalAxisX, ay = new Vector2(-ax.y, ax.x);
Vector2 hx = ax * s.Size.x * 0.5f, hy = ay * s.Size.y * 0.5f;
var v = new Vector3[] { s.Center - hx - hy, s.Center + hx - hy, s.Center + hx + hy, s.Center - hx + hy };
Handles.DrawSolidRectangleWithOutline(v, c, sel ? Color.yellow : Color.black);
break;
}
default:
if (s.Points != null && s.Points.Length >= 3)
{
var v = new Vector3[s.Points.Length];
for (int i = 0; i < v.Length; i++) v[i] = s.Points[i];
Handles.DrawAAConvexPolygon(v);
}
break;
}
Handles.Label(new Vector3(s.Center.x, s.Center.y, 0f), def != null ? def.nameKo : r.gimmickId);
}
}
}