Files
unity-zone-system/Editor/ZoneInstanceEditor.cs
Sebastian Bularca 823e146df0 copy from github
2026-03-27 15:13:27 +01:00

504 lines
21 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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.
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Jovian.ZoneSystem.Editor {
[CustomEditor(typeof(ZoneInstance))]
public class ZoneInstanceEditor : UnityEditor.Editor {
// Set to true externally to auto-enable editing when the inspector opens
internal static bool startEditingOnNextSelect;
private bool _editingPolygon;
private ZoneInstance _zone;
// ── Helpers ──────────────────────────────────────────────────────
private MapPlane ActivePlane {
get {
ZonesObjectHolder mgr = FindFirstObjectByType<ZonesObjectHolder>();
return mgr != null ? mgr.mapPlane : MapPlane.XZ;
}
}
private float DepthValue {
get {
MapPlane plane = ActivePlane;
return plane == MapPlane.XZ ? _zone.transform.position.y
: plane == MapPlane.YZ ? _zone.transform.position.x
: _zone.transform.position.z;
}
}
private void OnEnable() {
_zone = (ZoneInstance)target;
if(startEditingOnNextSelect) {
startEditingOnNextSelect = false;
_editingPolygon = true;
SceneView.RepaintAll();
}
}
private void OnDisable() {
_editingPolygon = false;
Tools.hidden = false;
}
// ── Scene GUI ────────────────────────────────────────────────────
private void OnSceneGUI() {
if(_zone.data == null) {
return;
}
// Hide the default transform handle when not editing the shape
if(!_editingPolygon) {
Tools.hidden = true;
} else {
Tools.hidden = false;
}
DrawFilledPolygon();
if(_editingPolygon) {
// Esc stops editing
if(Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Escape) {
_editingPolygon = false;
Event.current.Use();
Repaint();
SceneView.RepaintAll();
return;
}
// Consume default scene input so clicks don't select other objects
HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive));
if(_zone.data.shape == ZoneShape.Circle) {
DrawCircleRadiusHandle();
} else {
DrawVertexHandles();
HandleEdgeInsert();
}
}
}
private Vector2 PlaneOrigin => MapPlaneUtility.ProjectToPlane(_zone.transform.position, ActivePlane);
private Vector3 PolyPointToWorld(Vector2 pt) {
return MapPlaneUtility.UnprojectFromPlane(pt + PlaneOrigin, ActivePlane, DepthValue);
}
private Vector2 WorldToPolyPoint(Vector3 world) {
return MapPlaneUtility.ProjectToPlane(world, ActivePlane) - PlaneOrigin;
}
// ── Inspector ────────────────────────────────────────────────────
public override void OnInspectorGUI() {
DrawDefaultInspector();
EditorGUILayout.Space(8);
GUI.backgroundColor = new Color(0.4f, 0.7f, 1f);
if(GUILayout.Button("✏️ Edit in Zone Editor", GUILayout.Height(30))) {
ZoneEditorWindow.OpenAndEdit(_zone);
}
GUI.backgroundColor = Color.white;
EditorGUILayout.Space(4);
if(_zone.data == null) {
EditorGUILayout.HelpBox("Assign a ZoneData asset to begin editing.", MessageType.Warning);
return;
}
// Active plane info
EditorGUILayout.LabelField($"Active Plane: {ActivePlane} | Shape: {_zone.data.shape}",
new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = new Color(0.5f, 0.8f, 1f) } });
// ── Vertex List ─────────────────────────────────────────────
DrawVertexList();
// ── Shape Editing ───────────────────────────────────────────
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("Shape", EditorStyles.boldLabel);
GUI.backgroundColor = _editingPolygon ? new Color(0.4f, 0.9f, 0.4f) : Color.white;
if(GUILayout.Button(_editingPolygon ? "⬛ Stop Editing" : "✏️ Edit Shape", GUILayout.Height(27))) {
_editingPolygon = !_editingPolygon;
SceneView.RepaintAll();
}
GUI.backgroundColor = Color.white;
if(_editingPolygon) {
if(_zone.data.shape == ZoneShape.Circle) {
EditorGUILayout.HelpBox(
"• Drag the radius handle to resize the circle",
MessageType.Info);
} else {
EditorGUILayout.HelpBox(
"• Drag handles to move vertices\n" +
"• Ctrl+Click on an edge to insert a vertex\n" +
"• Shift+Click a vertex to delete it",
MessageType.Info);
}
}
if(_zone.data.shape == ZoneShape.Circle) {
EditorGUI.BeginChangeCheck();
float newRadius = EditorGUILayout.FloatField("Circle Radius", _zone.data.circleRadius);
if(EditorGUI.EndChangeCheck()) {
Undo.RecordObject(_zone.data, "Change Circle Radius");
_zone.data.circleRadius = Mathf.Max(0.1f, newRadius);
ShapeFactory.RegenerateCircle(_zone.data);
EditorUtility.SetDirty(_zone.data);
_zone.RebuildBoundsCache();
SceneView.RepaintAll();
}
}
EditorGUILayout.BeginHorizontal();
if(GUILayout.Button("⊕ Center Transform", GUILayout.Height(27))) {
RecenterTransformOnZone();
}
GUI.backgroundColor = new Color(1f, 0.7f, 0.3f);
if(GUILayout.Button("↺ Reset Shape", GUILayout.Height(27))) {
Undo.RecordObject(_zone.data, "Reset Zone Shape");
_zone.data.polygon.Clear();
_zone.data.polygon.AddRange(ShapeFactory.CreateDefault(_zone.data.shape));
if(_zone.data.shape == ZoneShape.Circle) {
_zone.data.circleRadius = ShapeFactory.DefaultRadius;
}
EditorUtility.SetDirty(_zone.data);
_zone.RebuildBoundsCache();
SceneView.RepaintAll();
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
// ── Duplication ─────────────────────────────────────────────
EditorGUILayout.Space(8);
if(GUILayout.Button("📋 Duplicate Zone", GUILayout.Height(27))) {
DuplicateZone();
}
// ── Summary ─────────────────────────────────────────────────
EditorGUILayout.Space(8);
DrawZoneSummary();
}
private void DrawZoneSummary() {
if(_zone.data == null) {
return;
}
ZoneData d = _zone.data;
Color roleColor = ZoneEditorSettings.FindOrCreateSettings().GetColorForRole(d.role);
Color prevBg = GUI.backgroundColor;
GUI.backgroundColor = roleColor * 2f;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUI.backgroundColor = prevBg;
EditorGUILayout.LabelField("Zone Summary", EditorStyles.boldLabel);
EditorGUILayout.LabelField($"Role: {d.role}");
EditorGUILayout.LabelField($"Priority: {d.priority}");
EditorGUILayout.LabelField($"Vertices: {d.polygon?.Count ?? 0}");
switch(d.role) {
case ZoneRole.Base:
EditorGUILayout.LabelField($"Tier: {d.baseDifficultyTier}");
EditorGUILayout.LabelField($"Chance: {d.baseEncounterChance:P0}");
EditorGUILayout.LabelField($"Table: {d.encounterTableId}");
break;
case ZoneRole.Modifier:
EditorGUILayout.LabelField($"Chance ×: {d.encounterChanceMultiplier:F2}");
EditorGUILayout.LabelField($"Tier +: {d.difficultyTierBonus}");
break;
case ZoneRole.Override:
EditorGUILayout.LabelField(d.isSafeZone
? "⚑ SAFE ZONE — no encounters"
: $"⚑ Override → Tier {d.overrideDifficultyTier}, {d.overrideEncounterChance:P0}");
break;
}
EditorGUILayout.EndVertical();
}
private bool _showVertexList;
private void DrawVertexList() {
List<Vector2> pts = _zone.data.polygon;
if(pts == null || pts.Count == 0) {
return;
}
EditorGUILayout.Space(4);
_showVertexList = EditorGUILayout.BeginFoldoutHeaderGroup(_showVertexList,
$"Vertices ({pts.Count})");
if(_showVertexList) {
GUIStyle miniStyle = new GUIStyle(EditorStyles.miniLabel) {
alignment = TextAnchor.MiddleLeft,
richText = true
};
for(int i = 0; i < pts.Count; i++) {
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(
$"<b>[{i}]</b> ({pts[i].x:F2}, {pts[i].y:F2})",
miniStyle);
if(GUILayout.Button("Copy", EditorStyles.miniButton, GUILayout.Width(40))) {
EditorGUIUtility.systemCopyBuffer = $"{pts[i].x:F2}, {pts[i].y:F2}";
}
EditorGUILayout.EndHorizontal();
}
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
private void DrawFilledPolygon() {
List<Vector2> pts = _zone.data.polygon;
if(pts == null || pts.Count < 3) {
return;
}
Color fill = _zone.data.debugColor;
Color border = new Color(fill.r, fill.g, fill.b, Mathf.Clamp01(fill.a * 3f));
Vector3[] verts = new Vector3[pts.Count];
for(int i = 0; i < pts.Count; i++) {
verts[i] = PolyPointToWorld(pts[i]);
}
// Triangulate to handle concave polygons correctly
List<int> tris = PolygonUtils.Triangulate(pts);
Handles.color = fill;
for(int i = 0; i + 2 < tris.Count; i += 3) {
Handles.DrawAAConvexPolygon(verts[tris[i]], verts[tris[i + 1]], verts[tris[i + 2]]);
}
Handles.color = border;
for(int i = 0; i < pts.Count; i++) {
Handles.DrawLine(verts[i], verts[(i + 1) % pts.Count], 2f);
}
// Zone label at centroid
Vector2 centroid2D = PolygonUtils.Centroid(pts);
Vector3 labelPos = PolyPointToWorld(centroid2D);
Handles.Label(labelPos, _zone.data.zoneName, new GUIStyle(EditorStyles.boldLabel) {
normal = { textColor = Color.white },
fontSize = 11
});
}
private void DrawCircleRadiusHandle() {
Vector2 center = PolygonUtils.Centroid(_zone.data.polygon);
Vector2 radiusPoint = center + new Vector2(_zone.data.circleRadius, 0f);
Vector3 worldRadiusPoint = PolyPointToWorld(radiusPoint);
float size = HandleUtility.GetHandleSize(worldRadiusPoint) * 0.1f;
Handles.color = Color.cyan;
EditorGUI.BeginChangeCheck();
Vector3 newWorld = Handles.FreeMoveHandle(worldRadiusPoint, size, Vector3.zero, Handles.DotHandleCap);
if(EditorGUI.EndChangeCheck()) {
Undo.RecordObject(_zone.data, "Change Circle Radius");
Vector2 newPlane = WorldToPolyPoint(newWorld);
float newRadius = Mathf.Max(0.1f, Vector2.Distance(center, newPlane));
_zone.data.circleRadius = newRadius;
ShapeFactory.RegenerateCircle(_zone.data);
EditorUtility.SetDirty(_zone.data);
_zone.RebuildBoundsCache();
}
}
private void DrawVertexHandles() {
List<Vector2> pts = _zone.data.polygon;
if(pts == null) {
return;
}
Event e = Event.current;
for(int i = 0; i < pts.Count; i++) {
Vector3 worldPos = PolyPointToWorld(pts[i]);
float size = HandleUtility.GetHandleSize(worldPos) * 0.08f;
// Shift+Click → delete vertex (minimum 3)
if(e.shift && e.type == EventType.MouseDown && e.button == 0) {
if(HandleUtility.DistanceToCircle(worldPos, size) < size && pts.Count > 3) {
Undo.RecordObject(_zone.data, "Delete Zone Vertex");
pts.RemoveAt(i);
EditorUtility.SetDirty(_zone.data);
_zone.RebuildBoundsCache();
e.Use();
return;
}
}
Handles.color = e.shift ? Color.red : Color.yellow;
EditorGUI.BeginChangeCheck();
Vector3 newWorld = Handles.FreeMoveHandle(worldPos, size, Vector3.zero, Handles.DotHandleCap);
if(EditorGUI.EndChangeCheck()) {
Undo.RecordObject(_zone.data, "Move Zone Vertex");
pts[i] = WorldToPolyPoint(newWorld);
EditorUtility.SetDirty(_zone.data);
_zone.RebuildBoundsCache();
}
}
}
private void HandleEdgeInsert() {
List<Vector2> pts = _zone.data.polygon;
if(pts == null || pts.Count < 2) {
return;
}
Event e = Event.current;
// Highlight the closest edge while Ctrl is held for visual feedback
if(e.control) {
int previewEdge = FindClosestEdge(pts, out _, out float previewDist);
if(previewEdge >= 0 && previewDist < 20f) {
Vector3 a = PolyPointToWorld(pts[previewEdge]);
Vector3 b = PolyPointToWorld(pts[(previewEdge + 1) % pts.Count]);
Handles.color = Color.cyan;
Handles.DrawLine(a, b, 4f);
HandleUtility.Repaint();
}
}
if(!e.control || e.type != EventType.MouseDown || e.button != 0) {
return;
}
// 20px screen-space tolerance — camera-distance independent
int bestEdge = FindClosestEdge(pts, out Vector3 insertPoint, out float dist);
if(bestEdge >= 0 && dist < 20f) {
Undo.RecordObject(_zone.data, "Insert Zone Vertex");
pts.Insert(bestEdge + 1, WorldToPolyPoint(insertPoint));
EditorUtility.SetDirty(_zone.data);
_zone.RebuildBoundsCache();
e.Use();
}
}
/// <summary>
/// Finds the polygon edge closest to the mouse in screen space (pixels).
/// Returns the edge index to insert after, the world-space insertion point, and pixel distance.
/// Screen-space comparison means tolerance is camera-distance independent.
/// </summary>
private int FindClosestEdge(List<Vector2> pts, out Vector3 closestPoint, out float closestPixelDist) {
closestPoint = Vector3.zero;
closestPixelDist = float.MaxValue;
int bestEdge = -1;
Vector2 mouseGUI = Event.current.mousePosition;
for(int i = 0; i < pts.Count; i++) {
Vector3 a = PolyPointToWorld(pts[i]);
Vector3 b = PolyPointToWorld(pts[(i + 1) % pts.Count]);
Vector2 aScreen = HandleUtility.WorldToGUIPoint(a);
Vector2 bScreen = HandleUtility.WorldToGUIPoint(b);
Vector2 ab = bScreen - aScreen;
float len = ab.sqrMagnitude;
float t = len > 0.0001f
? Mathf.Clamp01(Vector2.Dot(mouseGUI - aScreen, ab) / len)
: 0f;
float pixelDist = Vector2.Distance(mouseGUI, aScreen + (ab * t));
if(pixelDist < closestPixelDist) {
closestPixelDist = pixelDist;
bestEdge = i;
closestPoint = Vector3.Lerp(a, b, t);
}
}
return bestEdge;
}
// ── Re-center ────────────────────────────────────────────────────
private void RecenterTransformOnZone() {
List<Vector2> pts = _zone.data.polygon;
if(pts == null || pts.Count == 0) {
return;
}
Vector2 centroid = PolygonUtils.Centroid(pts);
if(centroid.sqrMagnitude < 0.001f) {
return;
}
Undo.RecordObject(_zone.data, "Center Transform on Zone");
Undo.RecordObject(_zone.transform, "Center Transform on Zone");
// Shift all polygon points so the centroid becomes (0,0)
for(int i = 0; i < pts.Count; i++) {
pts[i] -= centroid;
}
// Move the transform so the zone stays in the same world position
Vector3 worldOffset = MapPlaneUtility.UnprojectFromPlane(centroid, ActivePlane, 0f);
_zone.transform.position += worldOffset;
EditorUtility.SetDirty(_zone.data);
EditorUtility.SetDirty(_zone.transform);
_zone.RebuildBoundsCache();
SceneView.RepaintAll();
}
// ── Duplication ─────────────────────────────────────────────────
private void DuplicateZone() {
if(_zone.data == null) {
return;
}
ZoneData original = _zone.data;
ZoneEditorSettings settings = ZoneEditorSettings.FindOrCreateSettings();
string folder = settings.zoneDataFolder;
// Create independent ZoneData copy
ZoneData copy = ScriptableObject.CreateInstance<ZoneData>();
EditorUtility.CopySerialized(original, copy);
copy.zoneId = original.zoneId + "_" + System.Guid.NewGuid().ToString("N").Substring(0, 6);
copy.zoneName = original.zoneName + " (Copy)";
string newName = original.zoneName.Replace(" ", "_") + "_Copy";
string newPath = AssetDatabase.GenerateUniqueAssetPath(
System.IO.Path.Combine(folder, newName + ".asset"));
System.IO.Directory.CreateDirectory(
System.IO.Path.Combine(Application.dataPath, "..", folder));
AssetDatabase.CreateAsset(copy, newPath);
AssetDatabase.SaveAssets();
// Create scene GameObject
GameObject duplicate = Instantiate(_zone.gameObject, _zone.transform.parent);
duplicate.name = copy.zoneName;
Undo.RegisterCreatedObjectUndo(duplicate, "Duplicate Zone");
ZoneInstance dupInstance = duplicate.GetComponent<ZoneInstance>();
dupInstance.data = copy;
dupInstance.RebuildBoundsCache();
Vector3 offset = MapPlaneUtility.UnprojectFromPlane(new Vector2(1f, 1f), ActivePlane, 0f);
duplicate.transform.position += offset;
Selection.activeGameObject = duplicate;
SceneView.RepaintAll();
Debug.Log($"[ZoneSystem] Duplicated zone to {newPath}");
}
}
}
#endif