#if UNITY_EDITOR
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace Jovian.ZoneSystem.Editor {
///
/// Main Zone Editor window.
/// Open via: Window → Zone System → Zone Editor
///
public class ZoneEditorWindow : EditorWindow {
private string _exportPath = "Assets/StreamingAssets/zones.json";
// ── Create form state ───────────────────────────────────────────
private bool _showCreateForm;
private string _newZoneName = "New Zone";
private ZoneShape _newZoneShape = ZoneShape.Square;
// ── Edit state ──────────────────────────────────────────────────
private ZoneInstance _editingZone;
private SerializedObject _editingSO;
private bool _isUnsavedNewData;
private bool _hasUnsavedChanges;
private string _saveError;
private ZoneShape _shapeOnEditStart;
// ── Scroll / foldouts ───────────────────────────────────────────
private Vector2 _scrollPos;
private bool _showExportFoldout = true;
// ── GUI ──────────────────────────────────────────────────────────
private void OnGUI() {
DrawHeader();
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
if(_editingZone != null) {
DrawEditSection();
} else {
DrawCreateButton();
if(_showCreateForm) {
DrawCreateForm();
}
EditorGUILayout.Space(6);
DrawSceneZonesList();
}
EditorGUILayout.Space(6);
DrawExportSection();
EditorGUILayout.EndScrollView();
}
private void OnFocus() {
ValidateEditingState();
Repaint();
}
private void OnHierarchyChange() {
ValidateEditingState();
Repaint();
}
private void OnSelectionChange() {
ValidateEditingState();
Repaint();
}
private void ValidateEditingState() {
if(_editingZone == null && _editingSO != null) {
_editingSO = null;
}
}
[MenuItem("Window/Zone System/Zone Editor")]
public static void Open() {
GetWindow("Zone Editor");
}
public static void OpenAndEdit(ZoneInstance zone) {
ZoneEditorWindow window = GetWindow("Zone Editor");
window.EnterEditMode(zone);
}
// ── Header ──────────────────────────────────────────────────────
private void DrawHeader() {
Color prevBg = GUI.backgroundColor;
GUI.backgroundColor = new Color(0.2f, 0.2f, 0.3f);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUI.backgroundColor = prevBg;
EditorGUILayout.LabelField("🗺 Zone System Editor", new GUIStyle(EditorStyles.boldLabel) {
fontSize = 14,
normal = { textColor = new Color(0.8f, 0.9f, 1f) }
});
EditorGUILayout.LabelField("Define map zones for encounter difficulty and chance.",
EditorStyles.miniLabel);
EditorGUILayout.EndVertical();
EditorGUILayout.Space(4);
}
// ── Create Button + Dropdown ────────────────────────────────────
private void DrawCreateButton() {
GUI.backgroundColor = new Color(0.3f, 0.7f, 0.3f);
if(GUILayout.Button("➕ Create New Zone", GUILayout.Height(30))) {
_showCreateForm = !_showCreateForm;
}
GUI.backgroundColor = Color.white;
}
private void DrawCreateForm() {
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUI.indentLevel++;
_newZoneName = EditorGUILayout.TextField("Zone Name", _newZoneName);
_newZoneShape = (ZoneShape)EditorGUILayout.EnumPopup("Shape", _newZoneShape);
EditorGUI.indentLevel--;
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("All zone data can be edited after creation.",
EditorStyles.miniLabel);
if(GUILayout.Button("Create & Edit", GUILayout.Height(26))) {
CreateZoneInScene();
}
EditorGUILayout.EndVertical();
}
// ── Edit Section ────────────────────────────────────────────────
private void EnterEditMode(ZoneInstance zone) {
_editingZone = zone;
_editingSO = zone.data != null ? new SerializedObject(zone.data) : null;
_showCreateForm = false;
_shapeOnEditStart = zone.data != null ? zone.data.shape : ZoneShape.Polygon;
Selection.activeGameObject = zone.gameObject;
SceneView.FrameLastActiveSceneView();
ZoneInstanceEditor.startEditingOnNextSelect = true;
}
private void ExitEditMode() {
// If exiting with unsaved new data, clean up the in-memory asset
if(_isUnsavedNewData && _editingZone != null && _editingZone.data != null) {
DestroyImmediate(_editingZone.data);
_editingZone.data = null;
}
_editingZone = null;
_editingSO = null;
_isUnsavedNewData = false;
_hasUnsavedChanges = false;
_saveError = null;
}
private void DrawEditSection() {
// Validate that the zone still exists
if(_editingZone == null || _editingZone.data == null) {
ExitEditMode();
return;
}
// Back button
EditorGUILayout.BeginHorizontal();
Color prevBackBg = GUI.backgroundColor;
GUI.backgroundColor = new Color(0.9f, 0.3f, 0.3f);
if(GUILayout.Button("← Back", GUILayout.Height(36), GUILayout.Width(70))) {
ExitEditMode();
GUI.backgroundColor = prevBackBg;
EditorGUILayout.EndHorizontal();
return;
}
GUI.backgroundColor = prevBackBg;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(4);
ZoneData d = _editingZone.data;
// Zone header with color swatch
Color prevBg = GUI.backgroundColor;
GUI.backgroundColor = new Color(0.2f, 0.3f, 0.4f);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUI.backgroundColor = prevBg;
string headerLabel = _isUnsavedNewData ? $"New Zone: {d.zoneName}" : $"Editing: {d.zoneName}";
EditorGUILayout.LabelField(headerLabel, new GUIStyle(EditorStyles.boldLabel) {
fontSize = 13,
normal = { textColor = new Color(0.9f, 0.95f, 1f) }
});
EditorGUILayout.EndVertical();
EditorGUILayout.Space(4);
// Draw the ZoneData fields using SerializedObject
if(_editingSO == null) {
return;
}
_editingSO.Update();
// Identity
EditorGUILayout.LabelField("Identity", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_editingSO.FindProperty("zoneId"), new GUIContent("Zone ID"));
EditorGUILayout.PropertyField(_editingSO.FindProperty("zoneName"), new GUIContent("Zone Name"));
// Track role changes to auto-apply color
SerializedProperty rolePropForColor = _editingSO.FindProperty("role");
ZoneRole roleBefore = (ZoneRole)rolePropForColor.enumValueIndex;
EditorGUILayout.PropertyField(rolePropForColor, new GUIContent("Role"));
ZoneRole roleAfter = (ZoneRole)rolePropForColor.enumValueIndex;
if(roleBefore != roleAfter) {
ZoneEditorSettings settings = FindSettings();
_editingSO.FindProperty("debugColor").colorValue = settings.GetColorForRole(roleAfter);
}
EditorGUILayout.PropertyField(_editingSO.FindProperty("priority"), new GUIContent("Priority"));
EditorGUILayout.PropertyField(_editingSO.FindProperty("shape"), new GUIContent("Shape"));
SerializedProperty shapeProp = _editingSO.FindProperty("shape");
if((ZoneShape)shapeProp.enumValueIndex == ZoneShape.Circle) {
EditorGUILayout.PropertyField(_editingSO.FindProperty("circleRadius"), new GUIContent("Circle Radius"));
}
EditorGUILayout.Space(8);
// Role-specific fields
SerializedProperty roleProp = _editingSO.FindProperty("role");
ZoneRole role = (ZoneRole)roleProp.enumValueIndex;
switch(role) {
case ZoneRole.Base:
EditorGUILayout.LabelField("Base Zone Settings", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_editingSO.FindProperty("encounterTableId"), new GUIContent("Encounter Table ID"));
EditorGUILayout.PropertyField(_editingSO.FindProperty("baseDifficultyTier"), new GUIContent("Difficulty Tier"));
EditorGUILayout.PropertyField(_editingSO.FindProperty("baseEncounterChance"), new GUIContent("Encounter Chance"));
break;
case ZoneRole.Modifier:
EditorGUILayout.LabelField("Modifier Zone Settings", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_editingSO.FindProperty("encounterChanceMultiplier"), new GUIContent("Chance Multiplier"));
EditorGUILayout.PropertyField(_editingSO.FindProperty("difficultyTierBonus"), new GUIContent("Difficulty Tier Bonus"));
break;
case ZoneRole.Override:
EditorGUILayout.LabelField("Override Zone Settings", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_editingSO.FindProperty("isSafeZone"), new GUIContent("Is Safe Zone"));
if(!_editingSO.FindProperty("isSafeZone").boolValue) {
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_editingSO.FindProperty("overrideEncounterTableId"), new GUIContent("Encounter Table ID"));
EditorGUILayout.PropertyField(_editingSO.FindProperty("overrideEncounterChance"), new GUIContent("Encounter Chance"));
EditorGUILayout.PropertyField(_editingSO.FindProperty("overrideDifficultyTier"), new GUIContent("Difficulty Tier"));
EditorGUI.indentLevel--;
}
break;
}
if(_editingSO.ApplyModifiedProperties()) {
_hasUnsavedChanges = true;
}
EditorGUILayout.Space(8);
// Save button — shown for new unsaved zones or any modified existing zone
if(_isUnsavedNewData || _hasUnsavedChanges) {
// Show error if any
if(!string.IsNullOrEmpty(_saveError)) {
EditorGUILayout.HelpBox(_saveError, MessageType.Error);
}
GUI.backgroundColor = new Color(0.3f, 0.7f, 0.3f);
if(GUILayout.Button("💾 Save Zone", GUILayout.Height(30))) {
SaveZoneData();
}
GUI.backgroundColor = Color.white;
}
// Delete button at the bottom
EditorGUILayout.Space(4);
GUI.backgroundColor = new Color(0.9f, 0.3f, 0.3f);
if(GUILayout.Button("🗑 Delete Zone", GUILayout.Height(27))) {
ZoneInstance zone = _editingZone;
ExitEditMode();
DeleteZone(zone);
}
GUI.backgroundColor = Color.white;
}
// ── Create ──────────────────────────────────────────────────────
private static ZoneEditorSettings FindSettings() {
return ZoneEditorSettings.FindOrCreateSettings();
}
private void CreateZoneInScene() {
// Create in-memory ZoneData (saved when user clicks Save)
ZoneEditorSettings settings = FindSettings();
ZoneData data = CreateInstance();
data.zoneId = _newZoneName.ToLower().Replace(" ", "_");
data.zoneName = _newZoneName;
data.shape = _newZoneShape;
data.debugColor = settings.GetColorForRole(data.role);
data.polygon.AddRange(ShapeFactory.CreateDefault(_newZoneShape));
// Create the scene GameObject
GameObject go = new GameObject(_newZoneName);
Undo.RegisterCreatedObjectUndo(go, "Create Zone");
ZoneInstance inst = go.AddComponent();
inst.data = data;
// Try to parent under ZoneManager if it exists
ZonesObjectHolder mgr = FindFirstObjectByType();
if(mgr != null) {
go.transform.SetParent(mgr.transform, true);
}
_showCreateForm = false;
_isUnsavedNewData = true;
_saveError = null;
EnterEditMode(inst);
}
private void CreateDataForZone(ZoneInstance zone) {
string zoneName = zone.gameObject.name;
ZoneEditorSettings settings = FindSettings();
// Create in-memory ZoneData (not saved as asset yet)
ZoneData data = CreateInstance();
data.zoneId = zoneName.ToLower().Replace(" ", "_");
data.zoneName = zoneName;
data.shape = ZoneShape.Polygon;
data.debugColor = settings.GetColorForRole(data.role);
data.polygon.AddRange(ShapeFactory.CreateDefault(ZoneShape.Polygon));
zone.data = data;
_isUnsavedNewData = true;
_saveError = null;
EnterEditMode(zone);
}
private void SaveZoneData() {
ZoneData data = _editingZone.data;
string zoneId = data.zoneId;
string assetName = data.zoneName.Replace(" ", "_");
// Check for duplicate zoneId or asset name among existing ZoneData assets
string[] existingGuids = AssetDatabase.FindAssets("t:ZoneData");
foreach(string guid in existingGuids) {
string path = AssetDatabase.GUIDToAssetPath(guid);
ZoneData existing = AssetDatabase.LoadAssetAtPath(path);
if(existing == null || existing == data) {
continue;
}
if(existing.zoneId == zoneId) {
_saveError = $"A ZoneData asset with ID '{zoneId}' already exists at:\n{path}";
return;
}
string existingAssetName = Path.GetFileNameWithoutExtension(path);
if(existingAssetName == assetName) {
_saveError = $"A ZoneData asset named '{assetName}' already exists at:\n{path}";
return;
}
}
// Reset polygon if shape type changed since edit started
if(data.shape != _shapeOnEditStart) {
data.polygon.Clear();
data.polygon.AddRange(ShapeFactory.CreateDefault(data.shape));
if(data.shape == ZoneShape.Circle) {
data.circleRadius = ShapeFactory.DefaultRadius;
}
_editingZone.RebuildBoundsCache();
SceneView.RepaintAll();
}
_shapeOnEditStart = data.shape;
if(_isUnsavedNewData) {
// New zone — create the asset for the first time
ZoneEditorSettings settings = FindSettings();
string folder = settings != null ? settings.zoneDataFolder : "Assets/ZoneData";
string soPath = $"{folder}/{assetName}.asset";
string fullFolder = Path.GetFullPath(Path.Combine(Application.dataPath, "..", folder));
Directory.CreateDirectory(fullFolder);
AssetDatabase.CreateAsset(data, soPath);
AssetDatabase.SaveAssets();
// Rebuild the SerializedObject now that the asset is persisted
_editingSO = new SerializedObject(data);
_isUnsavedNewData = false;
Debug.Log($"[ZoneSystem] Created ZoneData '{data.zoneName}' at {soPath}");
}
else {
// Existing zone — rename asset if needed, then save
string currentPath = AssetDatabase.GetAssetPath(data);
string currentAssetName = Path.GetFileNameWithoutExtension(currentPath);
if(currentAssetName != assetName) {
string renameError = AssetDatabase.RenameAsset(currentPath, assetName);
if(!string.IsNullOrEmpty(renameError)) {
_saveError = $"Failed to rename asset: {renameError}";
return;
}
}
EditorUtility.SetDirty(data);
AssetDatabase.SaveAssets();
Debug.Log($"[ZoneSystem] Saved ZoneData '{data.zoneName}'");
}
// Rename the GameObject to match the zone name
Undo.RecordObject(_editingZone.gameObject, "Rename Zone GameObject");
_editingZone.gameObject.name = data.zoneName;
EditorUtility.SetDirty(_editingZone);
_hasUnsavedChanges = false;
_saveError = null;
}
// ── Scene Zones List ────────────────────────────────────────────
private void DrawSceneZonesList() {
EditorGUILayout.LabelField("Scene Zones", EditorStyles.boldLabel);
// Show active map plane from ZoneManager
ZonesObjectHolder mgr = FindFirstObjectByType();
if(mgr != null) {
EditorGUILayout.LabelField($"Map Plane: {mgr.mapPlane} (set on ZoneManager)",
new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = new Color(0.5f, 0.8f, 1f) } });
}
else {
EditorGUILayout.HelpBox("No ZoneManager found in scene.", MessageType.Warning);
}
ZoneInstance[] zones = FindObjectsByType(FindObjectsSortMode.None)
.OrderByDescending(z => z.data?.priority ?? 0)
.ThenBy(z => z.data?.zoneName ?? "")
.ToArray();
if(zones.Length == 0) {
EditorGUILayout.HelpBox("No ZoneInstance objects found in the scene.", MessageType.Info);
return;
}
foreach(ZoneInstance zone in zones) {
DrawZoneRow(zone);
}
}
private void DrawZoneRow(ZoneInstance zone) {
if(zone.data == null) {
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
// Warning icon
GUIContent warnIcon = EditorGUIUtility.IconContent("console.warnicon.sml");
EditorGUILayout.LabelField(warnIcon, GUILayout.Width(18), GUILayout.Height(20));
EditorGUILayout.LabelField($"{zone.gameObject.name}: Missing ZoneData", EditorStyles.miniLabel);
// Add & Edit button — creates a ZoneData asset and enters edit mode
if(GUILayout.Button("+ Add & Edit", GUILayout.Width(90), GUILayout.Height(20))) {
CreateDataForZone(zone);
}
GUI.backgroundColor = new Color(0.9f, 0.3f, 0.3f);
if(GUILayout.Button("✕", GUILayout.Width(28), GUILayout.Height(20))) {
if(EditorUtility.DisplayDialog("Delete Zone",
$"Delete '{zone.gameObject.name}'? (no ZoneData asset to remove)", "Delete", "Cancel")) {
Undo.DestroyObjectImmediate(zone.gameObject);
}
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
return;
}
ZoneData d = zone.data;
Color roleColor = FindSettings().GetColorForRole(d.role);
Color prevBg = GUI.backgroundColor;
GUI.backgroundColor = (roleColor * 2f * 0.6f) + (Color.gray * 0.4f);
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
GUI.backgroundColor = prevBg;
// Color swatch
Rect swatchRect = GUILayoutUtility.GetRect(12, 20, GUILayout.Width(12));
EditorGUI.DrawRect(swatchRect, roleColor * 3f);
// Info
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField($"{d.zoneName}", EditorStyles.boldLabel);
EditorGUILayout.LabelField(BuildZoneSummaryString(d), EditorStyles.miniLabel);
EditorGUILayout.EndVertical();
// Select / Edit button
if(GUILayout.Button("Select", GUILayout.Width(55), GUILayout.Height(36))) {
EnterEditMode(zone);
}
// Duplicate button
if(GUILayout.Button("📋", GUILayout.Width(28), GUILayout.Height(36))) {
DuplicateZone(zone);
}
// Delete button
GUI.backgroundColor = new Color(0.9f, 0.3f, 0.3f);
if(GUILayout.Button("✕", GUILayout.Width(28), GUILayout.Height(36))) {
DeleteZone(zone);
}
GUI.backgroundColor = prevBg;
EditorGUILayout.EndHorizontal();
}
private void DeleteZone(ZoneInstance zone) {
string zoneName = zone.data != null ? zone.data.zoneName : zone.gameObject.name;
string assetPath = zone.data != null ? AssetDatabase.GetAssetPath(zone.data) : null;
string message = $"Delete zone '{zoneName}'?";
if(!string.IsNullOrEmpty(assetPath)) {
message += $"\n\nThis will also delete the asset:\n{assetPath}";
}
if(!EditorUtility.DisplayDialog("Delete Zone", message, "Delete", "Cancel")) {
return;
}
if(!string.IsNullOrEmpty(assetPath)) {
AssetDatabase.DeleteAsset(assetPath);
}
Undo.DestroyObjectImmediate(zone.gameObject);
}
private void DuplicateZone(ZoneInstance zone) {
if(zone.data == null) {
return;
}
ZoneData original = zone.data;
ZoneEditorSettings settings = FindSettings();
string folder = settings.zoneDataFolder;
// Create independent ZoneData copy
ZoneData copy = 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(
Path.Combine(folder, newName + ".asset"));
string fullFolder = Path.GetFullPath(Path.Combine(Application.dataPath, "..", folder));
Directory.CreateDirectory(fullFolder);
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();
// Offset slightly so it's not on top of the original
ZonesObjectHolder mgr = FindFirstObjectByType();
MapPlane plane = mgr != null ? mgr.mapPlane : MapPlane.XZ;
Vector3 offset = MapPlaneUtility.UnprojectFromPlane(new Vector2(1f, 1f), plane, 0f);
duplicate.transform.position += offset;
Debug.Log($"[ZoneSystem] Duplicated zone to {newPath}");
}
private string BuildZoneSummaryString(ZoneData d) {
switch(d.role) {
case ZoneRole.Base:
return $"Base | Priority {d.priority} | {d.baseDifficultyTier} | {d.baseEncounterChance:P0} | Table: {d.encounterTableId}";
case ZoneRole.Modifier:
return $"Modifier | Priority {d.priority} | Chance ×{d.encounterChanceMultiplier:F2} | Tier +{d.difficultyTierBonus}";
case ZoneRole.Override:
return d.isSafeZone
? $"Override | Priority {d.priority} | ✓ SAFE"
: $"Override | Priority {d.priority} | {d.overrideDifficultyTier} | {d.overrideEncounterChance:P0}";
default: return "";
}
}
// ── Export Section ───────────────────────────────────────────────
private void DrawExportSection() {
_showExportFoldout = EditorGUILayout.BeginFoldoutHeaderGroup(_showExportFoldout, "Export Zones to JSON");
if(_showExportFoldout) {
EditorGUI.indentLevel++;
_exportPath = EditorGUILayout.TextField("Output Path", _exportPath);
EditorGUILayout.BeginHorizontal();
if(GUILayout.Button("Browse…", GUILayout.Width(70))) {
string picked = EditorUtility.SaveFilePanel(
"Save zones.json", Path.GetDirectoryName(_exportPath),
Path.GetFileName(_exportPath), "json");
if(!string.IsNullOrEmpty(picked)) {
_exportPath = "Assets" + picked.Substring(Application.dataPath.Length);
}
}
GUI.backgroundColor = new Color(0.4f, 0.8f, 0.4f);
if(GUILayout.Button("📦 Export Now", GUILayout.Height(24))) {
ExportZones();
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
private void ExportZones() {
ZoneInstance[] instances = FindObjectsByType(FindObjectsSortMode.None);
ZonesObjectHolder mgr = FindFirstObjectByType();
MapPlane plane = mgr != null ? mgr.mapPlane : MapPlane.XZ;
ZoneExportRoot root = ZoneExporter.BuildExport(instances, plane);
string json = ZoneExporter.ToJson(root);
string fullPath = Path.Combine(Application.dataPath, "../", _exportPath);
fullPath = Path.GetFullPath(fullPath);
Directory.CreateDirectory(Path.GetDirectoryName(fullPath));
File.WriteAllText(fullPath, json);
AssetDatabase.Refresh();
Debug.Log($"[ZoneSystem] Exported {root.zones.Count} zones → {fullPath}");
EditorUtility.DisplayDialog("Zone Export",
$"Successfully exported {root.zones.Count} zone(s) to:\n{_exportPath}", "OK");
}
}
}
#endif