copy from github

This commit is contained in:
Sebastian Bularca
2026-03-27 15:13:27 +01:00
parent 83532f396d
commit 823e146df0
44 changed files with 2999 additions and 2 deletions

View File

@@ -0,0 +1,503 @@
#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