copy from github
This commit is contained in:
18
Editor/Jovian.ZoneSystem.Editor.asmdef
Normal file
18
Editor/Jovian.ZoneSystem.Editor.asmdef
Normal 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
|
||||
}
|
||||
7
Editor/Jovian.ZoneSystem.Editor.asmdef.meta
Normal file
7
Editor/Jovian.ZoneSystem.Editor.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 18e660fb45b16f646be8417e3f101d98
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
131
Editor/ZoneDataEditor.cs
Normal file
131
Editor/ZoneDataEditor.cs
Normal 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
|
||||
2
Editor/ZoneDataEditor.cs.meta
Normal file
2
Editor/ZoneDataEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba5e43b325f91cd45a86ee6fc860275f
|
||||
109
Editor/ZoneEditorSettings.cs
Normal file
109
Editor/ZoneEditorSettings.cs
Normal 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
|
||||
3
Editor/ZoneEditorSettings.cs.meta
Normal file
3
Editor/ZoneEditorSettings.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 64856826ade04f41963e973ab19b2f00
|
||||
timeCreated: 1772984016
|
||||
53
Editor/ZoneEditorSettingsEditor.cs
Normal file
53
Editor/ZoneEditorSettingsEditor.cs
Normal 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
|
||||
2
Editor/ZoneEditorSettingsEditor.cs.meta
Normal file
2
Editor/ZoneEditorSettingsEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07d9ebf920c798c46b91e4f371ba5c7a
|
||||
643
Editor/ZoneEditorWindow.cs
Normal file
643
Editor/ZoneEditorWindow.cs
Normal 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
|
||||
2
Editor/ZoneEditorWindow.cs.meta
Normal file
2
Editor/ZoneEditorWindow.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f1ef1e3c20db2e4a904ef5201d403ec
|
||||
503
Editor/ZoneInstanceEditor.cs
Normal file
503
Editor/ZoneInstanceEditor.cs
Normal 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
|
||||
2
Editor/ZoneInstanceEditor.cs.meta
Normal file
2
Editor/ZoneInstanceEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ea29e8d3bc24ec49bdc2b279aaae4e9
|
||||
Reference in New Issue
Block a user