295 lines
13 KiB
C#
295 lines
13 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|