ThreeKingdomsSlingshot/Assets/Scripts/Editor/GimmickPainter.cs

295 lines
13 KiB
C#
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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