First commit on my server, yey!

This commit is contained in:
Sebastian Bularca
2026-03-19 18:12:07 +01:00
parent 5139ec2cec
commit fedd1961a0
602 changed files with 101587 additions and 6 deletions

View File

@@ -0,0 +1,18 @@
{
"name": "Jovian.ZoneSystem.Editor",
"rootNamespace": "Jovian.ZoneSystem.Editor",
"references": [
"Jovian.ZoneSystem"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 18e660fb45b16f646be8417e3f101d98
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,131 @@
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
namespace Jovian.ZoneSystem.Editor {
/// <summary>
/// Custom inspector for ZoneData ScriptableObject.
/// Shows only fields relevant to the selected ZoneRole.
/// </summary>
[CustomEditor(typeof(ZoneData))]
public class ZoneDataEditor : UnityEditor.Editor {
// Modifier
private SerializedProperty _chanceMultiplier, _tierBonus;
// Base
private SerializedProperty _encounterTableId, _baseDifficultyTier, _baseEncounterChance;
// Override
private SerializedProperty _isSafeZone, _overrideTableId, _overrideChance, _overrideTier;
private SerializedProperty _zoneId, _zoneName, _role, _priority, _debugColor;
private SerializedProperty _shape, _circleRadius;
private void OnEnable() {
_zoneId = serializedObject.FindProperty("zoneId");
_zoneName = serializedObject.FindProperty("zoneName");
_role = serializedObject.FindProperty("role");
_priority = serializedObject.FindProperty("priority");
_debugColor = serializedObject.FindProperty("debugColor");
_shape = serializedObject.FindProperty("shape");
_circleRadius = serializedObject.FindProperty("circleRadius");
_encounterTableId = serializedObject.FindProperty("encounterTableId");
_baseDifficultyTier = serializedObject.FindProperty("baseDifficultyTier");
_baseEncounterChance = serializedObject.FindProperty("baseEncounterChance");
_chanceMultiplier = serializedObject.FindProperty("encounterChanceMultiplier");
_tierBonus = serializedObject.FindProperty("difficultyTierBonus");
_isSafeZone = serializedObject.FindProperty("isSafeZone");
_overrideTableId = serializedObject.FindProperty("overrideEncounterTableId");
_overrideChance = serializedObject.FindProperty("overrideEncounterChance");
_overrideTier = serializedObject.FindProperty("overrideDifficultyTier");
}
public override void OnInspectorGUI() {
serializedObject.Update();
// Identity
EditorGUILayout.LabelField("Identity", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_zoneId, new GUIContent("Zone ID"));
EditorGUILayout.PropertyField(_zoneName, new GUIContent("Zone Name"));
// Track role changes to auto-apply color
ZoneRole roleBefore = (ZoneRole)_role.enumValueIndex;
EditorGUILayout.PropertyField(_role, new GUIContent("Role"));
ZoneRole roleAfter = (ZoneRole)_role.enumValueIndex;
if(roleBefore != roleAfter) {
ZoneEditorSettings settings = ZoneEditorSettings.FindOrCreateSettings();
_debugColor.colorValue = settings.GetColorForRole(roleAfter);
}
EditorGUILayout.PropertyField(_priority, new GUIContent("Priority"));
EditorGUILayout.PropertyField(_shape, new GUIContent("Shape"));
if((ZoneShape)_shape.enumValueIndex == ZoneShape.Circle) {
EditorGUILayout.PropertyField(_circleRadius, new GUIContent("Circle Radius"));
}
EditorGUILayout.Space(8);
// Role-specific fields
ZoneRole role = (ZoneRole)_role.enumValueIndex;
switch(role) {
case ZoneRole.Base:
DrawBaseFields();
break;
case ZoneRole.Modifier:
DrawModifierFields();
break;
case ZoneRole.Override:
DrawOverrideFields();
break;
}
serializedObject.ApplyModifiedProperties();
}
private void DrawBaseFields() {
EditorGUILayout.LabelField("Base Zone Settings", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"Base zones define the encounter table and baseline difficulty. " +
"Only the highest-priority Base zone at a position is used.",
MessageType.None);
EditorGUILayout.PropertyField(_encounterTableId, new GUIContent("Encounter Table ID"));
EditorGUILayout.PropertyField(_baseDifficultyTier, new GUIContent("Difficulty Tier"));
EditorGUILayout.PropertyField(_baseEncounterChance, new GUIContent("Encounter Chance"));
}
private void DrawModifierFields() {
EditorGUILayout.LabelField("Modifier Zone Settings", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"Modifier zones adjust an overlapping Base zone's values multiplicatively. " +
"All Modifier zones at a position are stacked.",
MessageType.None);
EditorGUILayout.PropertyField(_chanceMultiplier, new GUIContent("Chance Multiplier"));
EditorGUILayout.PropertyField(_tierBonus, new GUIContent("Difficulty Tier Bonus"));
}
private void DrawOverrideFields() {
EditorGUILayout.LabelField("Override Zone Settings", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"Override zones completely replace all other zones at this position. " +
"Useful for story events, towns, and safe areas. " +
"Highest-priority Override wins if multiple are present.",
MessageType.None);
EditorGUILayout.PropertyField(_isSafeZone, new GUIContent("Is Safe Zone"));
if(!_isSafeZone.boolValue) {
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_overrideTableId, new GUIContent("Encounter Table ID"));
EditorGUILayout.PropertyField(_overrideChance, new GUIContent("Encounter Chance"));
EditorGUILayout.PropertyField(_overrideTier, new GUIContent("Difficulty Tier"));
EditorGUI.indentLevel--;
}
}
}
}
#endif

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ba5e43b325f91cd45a86ee6fc860275f

View File

@@ -0,0 +1,109 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Jovian.ZoneSystem.Editor {
[CreateAssetMenu(fileName = "ZoneEditorSettings", menuName = "Jovian/ZoneSystem/Zone Editor Settings")]
public class ZoneEditorSettings : ScriptableObject {
[Tooltip("Which two world axes your map lies on. Match this to your map's plane.")]
public MapPlane mapPlane = MapPlane.XZ;
[Tooltip("Folder path where new ZoneData assets are saved (relative to project root).")]
public string zoneDataFolder = "Assets/ZoneData";
[Tooltip("Debug color for each zone role. Add entries for any new roles.")]
public List<ZoneRoleColor> roleColors = new() {
new ZoneRoleColor { role = ZoneRole.Base, color = new Color(0.2f, 0.6f, 1f, 0.25f) },
new ZoneRoleColor { role = ZoneRole.Modifier, color = new Color(1f, 0.8f, 0.2f, 0.25f) },
new ZoneRoleColor { role = ZoneRole.Override, color = new Color(0.3f, 0.9f, 0.3f, 0.25f) }
};
public Color GetColorForRole(ZoneRole role) {
foreach(ZoneRoleColor entry in roleColors) {
if(entry.role == role) {
return entry.color;
}
}
return new Color(1f, 0.5f, 0f, 0.25f);
}
/// <summary>
/// Ensures every ZoneRole enum value has a color entry.
/// Call this after adding new roles to the enum.
/// </summary>
public void SyncRoleEntries() {
ZoneRole[] allRoles = (ZoneRole[])Enum.GetValues(typeof(ZoneRole));
foreach(ZoneRole role in allRoles) {
bool found = false;
foreach(ZoneRoleColor entry in roleColors) {
if(entry.role == role) {
found = true;
break;
}
}
if(!found) {
roleColors.Add(new ZoneRoleColor {
role = role,
color = new Color(0.5f, 0.5f, 0.5f, 0.25f)
});
}
}
}
[MenuItem("Window/Zone System/Settings")]
private static void SelectOrCreateSettings() {
ZoneEditorSettings settings = FindOrCreateSettings();
Selection.activeObject = settings;
EditorGUIUtility.PingObject(settings);
}
[MenuItem("Window/Zone System/Documentation")]
private static void OpenDocumentation() {
// Find the Documentation~ folder relative to this package
string[] guids = AssetDatabase.FindAssets("t:Script ZoneEditorSettings");
string packagePath = "Packages/com.jovian.zonesystem";
if(guids.Length > 0) {
string scriptPath = AssetDatabase.GUIDToAssetPath(guids[0]);
// scriptPath is like "Packages/com.jovian.zonesystem/Editor/ZoneEditorSettings.cs"
int editorIdx = scriptPath.IndexOf("/Editor/");
if(editorIdx >= 0) {
packagePath = scriptPath.Substring(0, editorIdx);
}
}
string fullPath = System.IO.Path.GetFullPath(
System.IO.Path.Combine(Application.dataPath, "..", packagePath, "Documentation~", "index.html"));
if(System.IO.File.Exists(fullPath)) {
Application.OpenURL("file:///" + fullPath.Replace("\\", "/"));
}
else {
Debug.LogWarning($"[ZoneSystem] Documentation not found at: {fullPath}");
}
}
internal static ZoneEditorSettings FindOrCreateSettings() {
string[] guids = AssetDatabase.FindAssets("t:ZoneEditorSettings");
if(guids.Length > 0) {
return AssetDatabase.LoadAssetAtPath<ZoneEditorSettings>(
AssetDatabase.GUIDToAssetPath(guids[0]));
}
// Create a new settings asset
string folder = "Assets";
ZoneEditorSettings newSettings = CreateInstance<ZoneEditorSettings>();
newSettings.SyncRoleEntries();
AssetDatabase.CreateAsset(newSettings, $"{folder}/ZoneEditorSettings.asset");
AssetDatabase.SaveAssets();
Debug.Log("[ZoneSystem] Created ZoneEditorSettings at Assets/ZoneEditorSettings.asset");
return newSettings;
}
}
[Serializable]
public struct ZoneRoleColor {
public ZoneRole role;
public Color color;
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 64856826ade04f41963e973ab19b2f00
timeCreated: 1772984016

View File

@@ -0,0 +1,53 @@
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
namespace Jovian.ZoneSystem.Editor {
[CustomEditor(typeof(ZoneEditorSettings))]
public class ZoneEditorSettingsEditor : UnityEditor.Editor {
public override void OnInspectorGUI() {
serializedObject.Update();
EditorGUI.BeginChangeCheck();
DrawDefaultInspector();
bool changed = EditorGUI.EndChangeCheck();
if(changed) {
serializedObject.ApplyModifiedProperties();
ApplyColorsToAllZoneData((ZoneEditorSettings)target);
}
EditorGUILayout.Space(8);
if(GUILayout.Button("Apply Colors to All Zones")) {
ApplyColorsToAllZoneData((ZoneEditorSettings)target);
}
}
private static void ApplyColorsToAllZoneData(ZoneEditorSettings settings) {
string[] guids = AssetDatabase.FindAssets("t:ZoneData");
int updated = 0;
foreach(string guid in guids) {
string path = AssetDatabase.GUIDToAssetPath(guid);
ZoneData data = AssetDatabase.LoadAssetAtPath<ZoneData>(path);
if(data == null) {
continue;
}
Color newColor = settings.GetColorForRole(data.role);
if(data.debugColor != newColor) {
Undo.RecordObject(data, "Update Zone Color");
data.debugColor = newColor;
EditorUtility.SetDirty(data);
updated++;
}
}
if(updated > 0) {
AssetDatabase.SaveAssets();
SceneView.RepaintAll();
}
}
}
}
#endif

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 07d9ebf920c798c46b91e4f371ba5c7a

View File

@@ -0,0 +1,643 @@
#if UNITY_EDITOR
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace Jovian.ZoneSystem.Editor {
/// <summary>
/// Main Zone Editor window.
/// Open via: Window → Zone System → Zone Editor
/// </summary>
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<ZoneEditorWindow>("Zone Editor");
}
public static void OpenAndEdit(ZoneInstance zone) {
ZoneEditorWindow window = GetWindow<ZoneEditorWindow>("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<ZoneData>();
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<ZoneInstance>();
inst.data = data;
// Try to parent under ZoneManager if it exists
ZonesObjectHolder mgr = FindFirstObjectByType<ZonesObjectHolder>();
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<ZoneData>();
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<ZoneData>(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<ZonesObjectHolder>();
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<ZoneInstance>(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<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(
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<ZoneInstance>();
dupInstance.data = copy;
dupInstance.RebuildBoundsCache();
// Offset slightly so it's not on top of the original
ZonesObjectHolder mgr = FindFirstObjectByType<ZonesObjectHolder>();
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<ZoneInstance>(FindObjectsSortMode.None);
ZonesObjectHolder mgr = FindFirstObjectByType<ZonesObjectHolder>();
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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8f1ef1e3c20db2e4a904ef5201d403ec

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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3ea29e8d3bc24ec49bdc2b279aaae4e9