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