using System.Collections.Generic; using System.IO; using System.Text; using UnityEditor; using UnityEngine; namespace TKS.EditorTools { /// /// 지형 기믹 배치 도구(PD #1097 §1 「어떤 맵이든 올려서 배치」) — 메뉴 TKS/Gimmick Painter. /// 스테이지 고르기 → 불러오기(stage_gimmicks.csv 의 그 스테이지 행) → 기믹·모양 고르고 「그리기」 → 씬 뷰에서 드래그(원 = 중심→반지름 · 사각 = 모서리→모서리) → /// 저장(그 스테이지 행만 바꾸고 다른 스테이지 행은 그대로 · utf-8-sig) → 자동 임포트(Play 중이면 레이어 다시 만듦). 표를 바꿨으면 기획 tables/ 에도 같은 파일을 둔다. /// 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 rows = new List(); string loadedStage = ""; bool dragging; Vector2 dragStart, dragNow; [MenuItem("TKS/Gimmick Painter (기믹 배치)")] public static void Open() { GetWindow("기믹 배치"); } void OnEnable() { SceneView.duringSceneGui += OnScene; } void OnDisable() { SceneView.duringSceneGui -= OnScene; } static BalanceConfig Cfg { get { return AssetDatabase.LoadAssetAtPath(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 { 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); } } }