- 桥梁与静态物体高度采样修复: - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程 - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题 - 新增 test_bridge_height_parity.gd 自动化对拍测试 - 40250 怪物击杀经验动效: - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附 - 40250 客户端全系统功能对齐(Batches 1-31): - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试 - 文档沉淀: - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
133 lines
5.1 KiB
GDScript
133 lines
5.1 KiB
GDScript
# atlas_navigation_system.gd —— 40250 Atlas M 键全景大地图与 NPC 寻路导航光标系统 1:1
|
|
# 严格对照:
|
|
# Client/Eternexus/root/uiminimap.py (AtlasWindow, MiniMap)
|
|
# ClientVS22/source/UserInterface/PythonMiniMap.cpp, PythonMiniMap.h (AddSignalPoint, ClearAllSignalPoint)
|
|
# Client/Eternexus/root/atlasinfo.txt
|
|
extends RefCounted
|
|
|
|
# 地图手绘全景 Atlas 纹理与尺寸映射
|
|
const MAP_ATLAS_REGISTRY := {
|
|
"metin2_map_a1": { "name": "赤蛇国主城 (Yongan)", "size": Vector2(256, 256), "world_size": Vector2(4000, 4000) },
|
|
"metin2_map_b1": { "name": "白虎国主城 (Joan)", "size": Vector2(256, 256), "world_size": Vector2(4000, 4000) },
|
|
"metin2_map_c1": { "name": "苍龙国主城 (Pyungmoo)", "size": Vector2(256, 256), "world_size": Vector2(4000, 4000) },
|
|
"metin2_map_milgyo": { "name": "幽冥神殿 (Dark Temple)", "size": Vector2(256, 256), "world_size": Vector2(2000, 2000) },
|
|
"metin2_map_deviltower": { "name": "恶魔之塔 (Demon Tower)", "size": Vector2(256, 256), "world_size": Vector2(1000, 1000) },
|
|
}
|
|
|
|
# 官方预设标准城镇 NPC 索引目录 (NPC VID -> 数据)
|
|
const PRESET_TOWN_NPCS := [
|
|
{ "vid": 20016, "name": "铁匠 (Blacksmith)", "category": "Smith", "pos": Vector2(580, 620) },
|
|
{ "vid": 9001, "name": "武器商人 (Weapon Dealer)", "category": "Shop", "pos": Vector2(610, 630) },
|
|
{ "vid": 9002, "name": "防具商人 (Armor Dealer)", "category": "Shop", "pos": Vector2(590, 650) },
|
|
{ "vid": 9003, "name": "杂货商人 (General Store)", "category": "Shop", "pos": Vector2(630, 600) },
|
|
{ "vid": 9005, "name": "仓库保管员 (Storekeeper)", "category": "Safebox", "pos": Vector2(550, 580) },
|
|
{ "vid": 20084, "name": "生物学家采吉拉布 (Biologist)", "category": "Quest", "pos": Vector2(680, 520) },
|
|
{ "vid": 20092, "name": "老行商 (Old Wanderer)", "category": "Special", "pos": Vector2(450, 710) },
|
|
{ "vid": 20008, "name": "城门守卫 (City Guard)", "category": "Guard", "pos": Vector2(800, 800) },
|
|
{ "vid": 20001, "name": "药店掌柜 (Alchemist)", "category": "Craft", "pos": Vector2(520, 640) },
|
|
{ "vid": 20091, "name": "修仙道长 (Taoist)", "category": "Skill", "pos": Vector2(640, 750) },
|
|
]
|
|
|
|
# 状态变量
|
|
var is_atlas_visible: bool = false
|
|
var current_map_id: String = "metin2_map_a1"
|
|
var zoom_level: float = 1.0 # 滚轮缩放倍率 (0.5x ~ 3.0x)
|
|
var pan_offset: Vector2 = Vector2.ZERO # 拖拽平移偏移
|
|
|
|
# 动态信标点列表 (1:1 CPythonMiniMap::m_SignalPointVector)
|
|
var signal_points: Array = []
|
|
|
|
func _init(map_id: String = "metin2_map_a1") -> void:
|
|
current_map_id = map_id
|
|
is_atlas_visible = false
|
|
zoom_level = 1.0
|
|
pan_offset = Vector2.ZERO
|
|
signal_points.clear()
|
|
|
|
# 1. M 键快捷切换全景大地图 (1:1 ToggleAtlasWindow)
|
|
func toggle_atlas() -> bool:
|
|
is_atlas_visible = !is_atlas_visible
|
|
return is_atlas_visible
|
|
|
|
func show_atlas() -> void:
|
|
is_atlas_visible = true
|
|
|
|
func hide_atlas() -> void:
|
|
is_atlas_visible = false
|
|
|
|
# 2. 切换当前地图与底图载入 (SetMapName)
|
|
func set_map(map_id: String) -> bool:
|
|
if not MAP_ATLAS_REGISTRY.has(map_id):
|
|
return false
|
|
current_map_id = map_id
|
|
clear_all_signals()
|
|
pan_offset = Vector2.ZERO
|
|
zoom_level = 1.0
|
|
return true
|
|
|
|
func get_current_map_info() -> Dictionary:
|
|
return MAP_ATLAS_REGISTRY.get(current_map_id, {})
|
|
|
|
# 3. 缩放与平移控制
|
|
func set_zoom(zoom: float) -> void:
|
|
zoom_level = clamp(zoom, 0.5, 3.0)
|
|
|
|
func apply_pan(delta_offset: Vector2) -> void:
|
|
pan_offset += delta_offset
|
|
|
|
# 4. NPC 目录检索与过滤
|
|
func get_npcs(category_filter: String = "") -> Array:
|
|
if category_filter.is_empty():
|
|
return PRESET_TOWN_NPCS
|
|
var filtered: Array = []
|
|
for npc in PRESET_TOWN_NPCS:
|
|
if npc.get("category") == category_filter:
|
|
filtered.append(npc)
|
|
return filtered
|
|
|
|
func find_npc_by_vid(vid: int) -> Dictionary:
|
|
for npc in PRESET_TOWN_NPCS:
|
|
if npc.get("vid") == vid:
|
|
return npc
|
|
return {}
|
|
|
|
# 5. 信标寻路导航点机制 (1:1 CPythonMiniMap::AddSignalPoint / ClearAllSignalPoint)
|
|
func add_signal_point(target_x: float, target_y: float) -> Dictionary:
|
|
var pt := {
|
|
"x": target_x,
|
|
"y": target_y,
|
|
"active": true
|
|
}
|
|
signal_points.append(pt)
|
|
return pt
|
|
|
|
func clear_all_signals() -> void:
|
|
signal_points.clear()
|
|
|
|
# 6. 指南针方位角与距离计算 (角色与目标间的连线向量)
|
|
func calculate_navigation_vector(player_pos: Vector2, target_pos: Vector2) -> Dictionary:
|
|
var diff := target_pos - player_pos
|
|
var dist := diff.length()
|
|
var angle_rad := diff.angle()
|
|
var angle_deg := rad_to_deg(angle_rad)
|
|
|
|
return {
|
|
"distance": dist,
|
|
"direction_vector": diff.normalized() if dist > 0.001 else Vector2.ZERO,
|
|
"angle_rad": angle_rad,
|
|
"angle_deg": angle_deg,
|
|
"arrived": (dist <= 30.0) # 30 码内视为到达目标
|
|
}
|
|
|
|
# 7. 世界坐标至大地图 UI 局部像素坐标投影转换
|
|
func world_to_atlas_coord(world_pos: Vector2) -> Vector2:
|
|
var map_info := get_current_map_info()
|
|
var world_size: Vector2 = map_info.get("world_size", Vector2(4000, 4000))
|
|
var atlas_size: Vector2 = map_info.get("size", Vector2(256, 256))
|
|
|
|
var ratio_x := atlas_size.x / world_size.x
|
|
var ratio_y := atlas_size.y / world_size.y
|
|
|
|
var projected := Vector2(world_pos.x * ratio_x, world_pos.y * ratio_y)
|
|
return (projected * zoom_level) + pan_offset
|