using System.Collections.Generic;
using System.Linq;
using Cielonos.MainGame.Map;
using Sirenix.OdinInspector;
using UnityEngine;
#if UNITY_EDITOR
using Sirenix.Utilities.Editor;
using UnityEditor;
#endif
namespace Cielonos.MainGame.Map
{
///
/// 地图预览器,在 Inspector 中可视化生成并展示全联通网格地图。
///
[CreateAssetMenu(fileName = "MapPreviewer", menuName = "Cielonos/Map/MapPreviewer")]
public class MapPreviewer : SerializedScriptableObject
{
// ----------------------------------------------------------------
// 配置
// ----------------------------------------------------------------
[Title("配置")]
[Required("需要指定 MapGenerationConfig")]
public MapGenerationConfig config;
[Title("预览外观")]
[Range(300f, 1000f)]
public float previewSize = 600f;
[MinValue(6f), MaxValue(24f)]
public float nodeRadius = 12f;
[Range(1f, 6f)]
public float edgeThickness = 2.5f;
// ----------------------------------------------------------------
// 节点颜色映射
// ----------------------------------------------------------------
[Title("节点颜色")]
public Color colorStart = new Color(0.4f, 0.9f, 0.4f);
public Color colorNormalCombat = new Color(0.85f, 0.35f, 0.35f);
public Color colorEliteCombat = new Color(0.9f, 0.55f, 0.15f);
public Color colorBossCombat = new Color(0.7f, 0.15f, 0.85f);
public Color colorMechanicalTable = new Color(0.3f, 0.7f, 1.0f);
public Color colorLogisticsCenter = new Color(1.0f, 0.85f, 0.2f);
public Color colorMedicalStation = new Color(0.3f, 0.85f, 0.6f);
#if UNITY_EDITOR
// ----------------------------------------------------------------
// 运行时缓存(仅 Editor)
// ----------------------------------------------------------------
[HideInInspector]
public RunMapData previewData;
// ----------------------------------------------------------------
// 按钮
// ----------------------------------------------------------------
[Title("操作")]
[Button("生成随机地图", ButtonSizes.Large), GUIColor(0.4f, 0.85f, 0.5f)]
public void GeneratePreview()
{
if (config == null)
{
Debug.LogWarning("[MapPreviewer] 请先指定 MapGenerationConfig。");
return;
}
previewData = MapGenerator.Generate(config);
EditorUtility.SetDirty(this);
}
[Button("清空预览", ButtonSizes.Medium), GUIColor(0.85f, 0.4f, 0.4f)]
public void ClearPreview()
{
previewData = null;
EditorUtility.SetDirty(this);
}
// ----------------------------------------------------------------
// Inspector 绘制
// ----------------------------------------------------------------
[OnInspectorGUI]
private void DrawMapCanvas()
{
if (previewData == null || previewData.nodes == null || previewData.nodes.Count == 0)
{
SirenixEditorGUI.InfoMessageBox("尚未生成地图,请点击「生成随机地图」按钮。");
return;
}
GUILayout.Space(8f);
SirenixEditorGUI.Title("地图预览", $"共 {previewData.totalNodes} 个房间", TextAlignment.Center, horizontalLine: true);
GUILayout.Space(4f);
// 预留正方形画布
Rect canvasRect = GUILayoutUtility.GetRect(
GUIContent.none,
GUIStyle.none,
GUILayout.ExpandWidth(true),
GUILayout.Height(previewSize)
);
// 深色背景
SirenixEditorGUI.DrawSolidRect(canvasRect, new Color(0.12f, 0.12f, 0.15f, 1f));
// 计算屏幕坐标
Dictionary screenPositions = ComputeScreenPositions(canvasRect);
// 绘制连线(先画)
DrawEdges(screenPositions);
// 绘制节点
DrawNodes(screenPositions);
// 图例
DrawLegend(canvasRect);
// 统计信息
DrawStats(canvasRect);
GUILayout.Space(4f);
}
// ----------------------------------------------------------------
// 坐标映射
// ----------------------------------------------------------------
private Dictionary ComputeScreenPositions(Rect canvas)
{
Dictionary result = new Dictionary();
if (previewData.nodes.Count == 0) return result;
// 找出网格坐标的边界
int minX = int.MaxValue, maxX = int.MinValue;
int minY = int.MaxValue, maxY = int.MinValue;
foreach (Vector2Int pos in previewData.nodes.Keys)
{
if (pos.x < minX) minX = pos.x;
if (pos.x > maxX) maxX = pos.x;
if (pos.y < minY) minY = pos.y;
if (pos.y > maxY) maxY = pos.y;
}
int gridWidth = Mathf.Max(maxX - minX, 1);
int gridHeight = Mathf.Max(maxY - minY, 1);
const float paddingRatio = 0.08f;
float padX = canvas.width * paddingRatio;
float padY = canvas.height * paddingRatio;
float cellW = (canvas.width - padX * 2f) / gridWidth;
float cellH = (canvas.height - padY * 2f) / gridHeight;
float cellSize = Mathf.Min(cellW, cellH);
// 居中偏移
float totalW = gridWidth * cellSize;
float totalH = gridHeight * cellSize;
float offsetX = canvas.x + (canvas.width - totalW) * 0.5f;
float offsetY = canvas.y + (canvas.height - totalH) * 0.5f;
foreach (Vector2Int pos in previewData.nodes.Keys)
{
float sx = offsetX + (pos.x - minX) * cellSize;
// Y 轴翻转:网格 Y 越大 → 画布 Y 越小(向上)
float sy = offsetY + (maxY - pos.y) * cellSize;
result[pos] = new Vector2(sx, sy);
}
return result;
}
// ----------------------------------------------------------------
// 连线绘制
// ----------------------------------------------------------------
private void DrawEdges(Dictionary positions)
{
if (Event.current.type != EventType.Repaint) return;
Color edgeColor = new Color(0.45f, 0.45f, 0.55f, 0.7f);
HashSet<(Vector2Int, Vector2Int)> drawnEdges = new HashSet<(Vector2Int, Vector2Int)>();
foreach (KeyValuePair kvp in previewData.nodes)
{
Vector2Int fromPos = kvp.Key;
if (!positions.TryGetValue(fromPos, out Vector2 fromScreen)) continue;
foreach (Vector2Int toPos in kvp.Value.connectedPositions)
{
// 避免双向连接绘制两次
var edgeKey = fromPos.x < toPos.x || (fromPos.x == toPos.x && fromPos.y < toPos.y)
? (fromPos, toPos)
: (toPos, fromPos);
if (drawnEdges.Contains(edgeKey)) continue;
drawnEdges.Add(edgeKey);
if (!positions.TryGetValue(toPos, out Vector2 toScreen)) continue;
DrawThickLine(fromScreen, toScreen, edgeColor, edgeThickness);
}
}
}
private static void DrawThickLine(Vector2 from, Vector2 to, Color color, float thickness)
{
Handles.color = color;
Vector2 dir = (to - from).normalized;
Vector2 perp = new Vector2(-dir.y, dir.x);
int steps = Mathf.Max(1, Mathf.RoundToInt(thickness));
float halfT = (steps - 1) * 0.5f;
for (int i = 0; i < steps; i++)
{
float offset = (i - halfT) * 0.8f;
Vector3 f3 = new Vector3(from.x + perp.x * offset, from.y + perp.y * offset, 0f);
Vector3 t3 = new Vector3(to.x + perp.x * offset, to.y + perp.y * offset, 0f);
Handles.DrawLine(f3, t3);
}
}
// ----------------------------------------------------------------
// 节点绘制
// ----------------------------------------------------------------
private void DrawNodes(Dictionary positions)
{
if (Event.current.type != EventType.Repaint) return;
foreach (KeyValuePair kvp in previewData.nodes)
{
if (!positions.TryGetValue(kvp.Key, out Vector2 center)) continue;
RunMapNode node = kvp.Value;
Color nodeColor = GetNodeColor(node.nodeType);
// 外圈(深色边框)
float outerR = nodeRadius + 2f;
Handles.color = new Color(0.05f, 0.05f, 0.05f, 0.9f);
Handles.DrawSolidDisc(new Vector3(center.x, center.y, 0f), Vector3.forward, outerR);
// 节点填充
Handles.color = nodeColor;
Handles.DrawSolidDisc(new Vector3(center.x, center.y, 0f), Vector3.forward, nodeRadius);
// 节点标签
DrawNodeLabel(node, center);
}
}
private void DrawNodeLabel(RunMapNode node, Vector2 center)
{
string label = GetNodeShortLabel(node.nodeType);
GUIStyle style = new GUIStyle(EditorStyles.miniLabel)
{
alignment = TextAnchor.MiddleCenter,
fontStyle = FontStyle.Bold,
fontSize = 9,
normal = { textColor = Color.white }
};
float labelW = 48f;
float labelH = 16f;
Rect labelRect = new Rect(center.x - labelW * 0.5f, center.y + nodeRadius + 2f, labelW, labelH);
GUI.Label(labelRect, label, style);
}
// ----------------------------------------------------------------
// 图例
// ----------------------------------------------------------------
private void DrawLegend(Rect canvas)
{
if (Event.current.type != EventType.Repaint) return;
var entries = new (MapNodeType type, string label)[]
{
(MapNodeType.Start, "起点"),
(MapNodeType.NormalCombat, "普通战斗"),
(MapNodeType.EliteCombat, "精英战斗"),
(MapNodeType.BossCombat, "Boss"),
(MapNodeType.MechanicalTable, "机械台"),
(MapNodeType.LogisticsCenter, "物流中心"),
(MapNodeType.MedicalStation, "医疗站"),
};
const float dotSize = 10f;
const float itemH = 16f;
const float itemW = 90f;
const float padLeft = 8f;
const float padBottom = 8f;
float totalH = entries.Length * itemH + 4f;
float startY = canvas.yMax - padBottom - totalH;
Rect bgRect = new Rect(canvas.x + padLeft - 4f, startY - 2f, itemW + 8f, totalH + 4f);
SirenixEditorGUI.DrawSolidRect(bgRect, new Color(0f, 0f, 0f, 0.45f));
GUIStyle labelStyle = new GUIStyle(EditorStyles.miniLabel)
{
normal = { textColor = new Color(0.85f, 0.85f, 0.85f) }
};
for (int i = 0; i < entries.Length; i++)
{
float y = startY + i * itemH;
Handles.color = GetNodeColor(entries[i].type);
Handles.DrawSolidDisc(
new Vector3(canvas.x + padLeft + dotSize * 0.5f, y + itemH * 0.5f, 0f),
Vector3.forward, dotSize * 0.5f);
Rect textRect = new Rect(canvas.x + padLeft + dotSize + 4f, y, itemW - dotSize - 4f, itemH);
GUI.Label(textRect, entries[i].label, labelStyle);
}
}
// ----------------------------------------------------------------
// 统计信息
// ----------------------------------------------------------------
private void DrawStats(Rect canvas)
{
if (Event.current.type != EventType.Repaint) return;
// 统计各类型数量
Dictionary counts = new Dictionary();
foreach (RunMapNode node in previewData.nodes.Values)
{
if (!counts.ContainsKey(node.nodeType))
counts[node.nodeType] = 0;
counts[node.nodeType]++;
}
string stats = string.Join(" | ", counts
.OrderBy(kvp => kvp.Key)
.Select(kvp => $"{GetNodeShortLabel(kvp.Key)}: {kvp.Value}"));
GUIStyle style = new GUIStyle(EditorStyles.miniLabel)
{
alignment = TextAnchor.MiddleRight,
normal = { textColor = new Color(0.7f, 0.7f, 0.7f) }
};
const float padRight = 8f;
const float padTop = 6f;
Rect statsRect = new Rect(canvas.x, canvas.y + padTop, canvas.width - padRight, 16f);
GUI.Label(statsRect, stats, style);
}
// ----------------------------------------------------------------
// 工具方法
// ----------------------------------------------------------------
private Color GetNodeColor(MapNodeType type)
{
return type switch
{
MapNodeType.Start => colorStart,
MapNodeType.NormalCombat => colorNormalCombat,
MapNodeType.EliteCombat => colorEliteCombat,
MapNodeType.BossCombat => colorBossCombat,
MapNodeType.MechanicalTable => colorMechanicalTable,
MapNodeType.LogisticsCenter => colorLogisticsCenter,
MapNodeType.MedicalStation => colorMedicalStation,
_ => Color.white,
};
}
private static string GetNodeShortLabel(MapNodeType type)
{
return type switch
{
MapNodeType.Start => "Start",
MapNodeType.NormalCombat => "Combat",
MapNodeType.EliteCombat => "Elite",
MapNodeType.BossCombat => "BOSS",
MapNodeType.MechanicalTable => "Chest",
MapNodeType.LogisticsCenter => "Shop",
MapNodeType.MedicalStation => "Med",
_ => "?",
};
}
#endif
}
}