feat: 添加可视化地图系统
- 创建地图布局工具(mapLayout.js) - 预定义区域位置配置 - BFS计算可达距离和最短路径 - 地图数据结构构建 - 添加迷你地图组件(MiniMap.vue) - 可视化显示所有区域节点和连接线 - 当前位置脉冲动画高亮 - 相邻区域虚线标记 - 锁定区域显示🔒图标 - 显示到各区域的距离步数 - 点击区域显示前往路径 - 图例说明不同区域类型 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
254
utils/mapLayout.js
Normal file
254
utils/mapLayout.js
Normal file
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* 地图布局系统 - 根据区域连接生成可视化地图
|
||||
*/
|
||||
|
||||
import { LOCATION_CONFIG } from '@/config/locations.js'
|
||||
|
||||
/**
|
||||
* 构建地图图形结构
|
||||
* @returns {Object} 地图数据
|
||||
*/
|
||||
export function buildMapGraph() {
|
||||
const nodes = []
|
||||
const edges = []
|
||||
|
||||
// 首先创建所有节点
|
||||
for (const [id, config] of Object.entries(LOCATION_CONFIG)) {
|
||||
nodes.push({
|
||||
id,
|
||||
name: config.name,
|
||||
type: config.type,
|
||||
x: 0,
|
||||
y: 0
|
||||
})
|
||||
}
|
||||
|
||||
// 创建连接边
|
||||
for (const [id, config] of Object.entries(LOCATION_CONFIG)) {
|
||||
if (config.connections) {
|
||||
for (const connId of config.connections) {
|
||||
// 避免重复边
|
||||
const edgeExists = edges.some(e =>
|
||||
(e.from === id && e.to === connId) ||
|
||||
(e.from === connId && e.to === id)
|
||||
)
|
||||
if (!edgeExists) {
|
||||
edges.push({ from: id, to: connId })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes, edges }
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的力导向布局算法
|
||||
* 将节点分布在合理的坐标上
|
||||
* @param {Object} graph - 地图图形结构
|
||||
* @param {Object} options - 布局选项
|
||||
* @returns {Object} 带有坐标的节点
|
||||
*/
|
||||
export function layoutMap(graph, options = {}) {
|
||||
const { nodes, edges } = graph
|
||||
const { width = 300, height = 400 } = options
|
||||
|
||||
// 创建邻接表
|
||||
const adj = {}
|
||||
for (const node of nodes) {
|
||||
adj[node.id] = []
|
||||
}
|
||||
for (const edge of edges) {
|
||||
adj[edge.from].push(edge.to)
|
||||
adj[edge.to].push(edge.from)
|
||||
}
|
||||
|
||||
// 找到中心节点(连接最多的节点作为起点)
|
||||
let centerNode = nodes[0]
|
||||
for (const node of nodes) {
|
||||
if (adj[node.id].length > adj[centerNode.id].length) {
|
||||
centerNode = node
|
||||
}
|
||||
}
|
||||
|
||||
// BFS 分层布局
|
||||
const levels = {}
|
||||
const visited = new Set()
|
||||
const queue = [{ id: centerNode.id, level: 0 }]
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { id, level } = queue.shift()
|
||||
if (visited.has(id)) continue
|
||||
visited.add(id)
|
||||
|
||||
if (!levels[level]) levels[level] = []
|
||||
levels[level].push(id)
|
||||
|
||||
for (const neighbor of adj[id]) {
|
||||
if (!visited.has(neighbor)) {
|
||||
queue.push({ id: neighbor, level: level + 1 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算节点位置
|
||||
const nodePositions = {}
|
||||
const levelHeight = height / (Object.keys(levels).length + 1)
|
||||
|
||||
for (const [level, nodeIds] of Object.entries(levels)) {
|
||||
const y = (parseInt(level) + 1) * levelHeight
|
||||
const levelWidth = width / (nodeIds.length + 1)
|
||||
|
||||
nodeIds.forEach((nodeId, index) => {
|
||||
nodePositions[nodeId] = {
|
||||
x: (index + 1) * levelWidth,
|
||||
y
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 更新节点坐标
|
||||
return nodes.map(node => ({
|
||||
...node,
|
||||
x: nodePositions[node.id]?.x || width / 2,
|
||||
y: nodePositions[node.id]?.y || height / 2
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取从当前位置可达的区域(包括多步可达)
|
||||
* @param {string} currentLocation - 当前位置ID
|
||||
* @param {number} maxDistance - 最大距离(步数)
|
||||
* @returns {Object} 可达区域信息,key为locationId,value为距离
|
||||
*/
|
||||
export function getReachableLocations(currentLocation, maxDistance = 10) {
|
||||
const distances = {}
|
||||
const queue = [{ id: currentLocation, dist: 0 }]
|
||||
|
||||
distances[currentLocation] = 0
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { id, dist } = queue.shift()
|
||||
|
||||
if (dist >= maxDistance) continue
|
||||
|
||||
// 找到所有连接的节点
|
||||
const connections = LOCATION_CONFIG[id]?.connections || []
|
||||
for (const connId of connections) {
|
||||
if (distances[connId] === undefined) {
|
||||
distances[connId] = dist + 1
|
||||
queue.push({ id: connId, dist: dist + 1 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return distances
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取地图的路径信息
|
||||
* @param {string} from - 起点
|
||||
* @param {string} to - 终点
|
||||
* @returns {Array} 路径数组
|
||||
*/
|
||||
export function getPath(from, to) {
|
||||
if (from === to) return [from]
|
||||
|
||||
const { edges } = buildMapGraph()
|
||||
const adj = {}
|
||||
for (const node of Object.keys(LOCATION_CONFIG)) {
|
||||
adj[node] = LOCATION_CONFIG[node]?.connections || []
|
||||
}
|
||||
|
||||
// BFS 寻找最短路径
|
||||
const queue = [[from]]
|
||||
const visited = new Set([from])
|
||||
|
||||
while (queue.length > 0) {
|
||||
const path = queue.shift()
|
||||
const current = path[path.length - 1]
|
||||
|
||||
if (current === to) {
|
||||
return path
|
||||
}
|
||||
|
||||
for (const neighbor of adj[current]) {
|
||||
if (!visited.has(neighbor)) {
|
||||
visited.add(neighbor)
|
||||
queue.push([...path, neighbor])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null // 无路径
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义的地图布局配置
|
||||
* 为每个区域指定固定的显示位置
|
||||
*/
|
||||
export const PREDEFINED_LAYOUT = {
|
||||
// 营地为中心
|
||||
camp: { x: 150, y: 200 },
|
||||
// 营地周围
|
||||
market: { x: 50, y: 120 },
|
||||
blackmarket: { x: 250, y: 120 },
|
||||
wild1: { x: 150, y: 320 },
|
||||
// 更远的地方
|
||||
boss_lair: { x: 150, y: 440 },
|
||||
basement: { x: 150, y: 540 }
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用预定义布局获取节点位置
|
||||
* @param {Object} graph - 地图图形结构
|
||||
* @returns {Array} 带有坐标的节点
|
||||
*/
|
||||
export function getPredefinedLayout(graph) {
|
||||
const { nodes } = graph
|
||||
|
||||
return nodes.map(node => ({
|
||||
...node,
|
||||
x: PREDEFINED_LAYOUT[node.id]?.x || 150,
|
||||
y: PREDEFINED_LAYOUT[node.id]?.y || 200
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可视化的地图数据
|
||||
* @param {string} currentLocation - 当前位置
|
||||
* @returns {Object} 地图数据
|
||||
*/
|
||||
export function getMapData(currentLocation) {
|
||||
const graph = buildMapGraph()
|
||||
const layoutNodes = getPredefinedLayout(graph)
|
||||
|
||||
// 计算画布大小
|
||||
let maxX = 0, maxY = 0
|
||||
for (const node of layoutNodes) {
|
||||
maxX = Math.max(maxX, node.x)
|
||||
maxY = Math.max(maxY, node.y)
|
||||
}
|
||||
|
||||
// 获取可达距离
|
||||
const reachable = getReachableLocations(currentLocation, 10)
|
||||
|
||||
// 计算到每个区域的路径
|
||||
const paths = {}
|
||||
for (const node of layoutNodes) {
|
||||
const path = getPath(currentLocation, node.id)
|
||||
if (path) {
|
||||
paths[node.id] = path
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: layoutNodes,
|
||||
edges: graph.edges,
|
||||
width: maxX + 50,
|
||||
height: maxY + 50,
|
||||
currentLocation,
|
||||
reachable,
|
||||
paths
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user