#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(); 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 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( $"[{i}] ({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 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 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 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 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(); } } /// /// 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. /// private int FindClosestEdge(List 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 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(); 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(); 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