# AtlasUI (P9) —— 对齐 CPythonMiniMap 的 Atlas 窗口:大地图、拖动、玩家 # 标记和坐标查询。地图底图复用每个区块的 minimap.dds;缺贴图时仍显示 # 可交互的中性底图,便于无资源/打包环境继续使用坐标查询。 # # var atlas := preload("res://ui/atlas_ui.gd").new() # atlas.setup(world, ui_manager, func() -> Node3D: return player, "map_a1") # atlas.toggle() # M 键 extends Node class AtlasOverlay extends Control: var owner_ui: Node func _draw() -> void: if owner_ui: owner_ui._draw_world_marks(self) const VIEW_SIZE := Vector2(720, 520) const CELL_PIXELS := 128 const MARK_SIZE := 8.0 var world: Node var client: Node var ui: CanvasLayer var player_getter: Callable var map_name := "" var _win: Control var _map_view: Control var _map: TextureRect var _overlay: Control var _player_mark: ColorRect var _coord: Label var _dragging := false var _drag_origin := Vector2.ZERO var _map_origin := Vector2.ZERO var _map_px := Vector2.ZERO var _world_size_m := Vector2(1, 1) func setup(metin_world: Node, ui_manager: CanvasLayer, get_player: Callable, name := "", m2client: Node = null) -> void: world = metin_world client = m2client ui = ui_manager player_getter = get_player map_name = name if name != "" else "地图" func is_open() -> bool: return _win != null and is_instance_valid(_win) func toggle() -> void: if is_open(): close() else: open() func open() -> void: if is_open() or ui == null: return _build_window() ui.open(_win, false) _update_player_marker() func close() -> void: if is_open() and ui: ui.close(_win) _win = null func reload_map(metin_world: Node, name := "") -> void: world = metin_world if name != "": map_name = name if is_open(): close() open() func _build_window() -> void: _win = Control.new() _win.name = "AtlasWindow" _win.set_anchors_preset(Control.PRESET_CENTER) _win.position = -VIEW_SIZE * 0.5 _win.size = VIEW_SIZE var panel := Panel.new() panel.set_anchors_preset(Control.PRESET_FULL_RECT) var style := StyleBoxFlat.new() style.bg_color = Color(0.055, 0.065, 0.09, 0.97) style.border_color = Color(0.4, 0.45, 0.58, 0.9) style.set_border_width_all(1) style.set_corner_radius_all(6) panel.add_theme_stylebox_override("panel", style) _win.add_child(panel) var title := Label.new() title.text = "Atlas · %s" % map_name title.position = Vector2(14, 8) title.add_theme_font_size_override("font_size", 15) _win.add_child(title) var close_btn := Button.new() close_btn.text = "×" close_btn.position = Vector2(VIEW_SIZE.x - 42, 5) close_btn.size = Vector2(32, 28) close_btn.pressed.connect(close) _win.add_child(close_btn) _coord = Label.new() _coord.position = Vector2(14, VIEW_SIZE.y - 30) _coord.text = "坐标:--" _coord.add_theme_font_size_override("font_size", 12) _win.add_child(_coord) _map_view = Control.new() _map_view.name = "MapView" _map_view.position = Vector2(14, 42) _map_view.size = Vector2(VIEW_SIZE.x - 28, VIEW_SIZE.y - 80) _map_view.clip_contents = true _map_view.gui_input.connect(_on_map_input) _win.add_child(_map_view) _overlay = AtlasOverlay.new() _overlay.owner_ui = self _overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE _map_view.add_child(_overlay) _build_map() func _build_map() -> void: var tiles := Vector2i(1, 1) if world and world.has_method("get_map_size_tiles"): tiles = world.get_map_size_tiles() tiles.x = maxi(1, tiles.x) tiles.y = maxi(1, tiles.y) _world_size_m = Vector2(tiles.x * 256.0, tiles.y * 256.0) _map_px = Vector2(tiles.x * CELL_PIXELS, tiles.y * CELL_PIXELS) var image := Image.create_empty(int(_map_px.x), int(_map_px.y), false, Image.FORMAT_RGBA8) image.fill(Color(0.12, 0.14, 0.18, 1.0)) var loaded := 0 for tx in tiles.x: for ty in tiles.y: if not world or not world.has_method("chunk_dir") or not world.has_method("load_dds"): continue var path: String = world.chunk_dir(tx, ty).path_join("minimap.dds") if not FileAccess.file_exists(path): continue var tile: Image = world.load_dds(path) if tile == null: continue tile.resize(CELL_PIXELS, CELL_PIXELS, Image.INTERPOLATE_BILINEAR) image.blit_rect(tile, Rect2i(Vector2i.ZERO, Vector2i(CELL_PIXELS, CELL_PIXELS)), Vector2i(tx * CELL_PIXELS, ty * CELL_PIXELS)) loaded += 1 var tex := ImageTexture.create_from_image(image) _map = TextureRect.new() _map.name = "AtlasTexture" _map.texture = tex _map.expand_mode = TextureRect.EXPAND_IGNORE_SIZE _map.stretch_mode = TextureRect.STRETCH_SCALE _map.size = _map_px _map.mouse_filter = Control.MOUSE_FILTER_IGNORE _map_origin = (_map_view.size - _map_px) * 0.5 _map.position = _map_origin _map_view.add_child(_map) _overlay.position = _map.position _overlay.size = _map.size _player_mark = ColorRect.new() _player_mark.name = "PlayerMark" _player_mark.color = Color(1.0, 0.85, 0.15, 1.0) _player_mark.size = Vector2(MARK_SIZE, MARK_SIZE) _player_mark.mouse_filter = Control.MOUSE_FILTER_IGNORE _overlay.add_child(_player_mark) _overlay.move_to_front() if loaded == 0: _coord.text = "坐标:--(缺 minimap.dds,仍可查询)" func _on_map_input(event: InputEvent) -> void: if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: _dragging = event.pressed if _dragging: _drag_origin = event.position _map_origin = _map.position _map_view.accept_event() elif event is InputEventMouseMotion and _dragging: _map.position = _map_origin + event.position - _drag_origin _overlay.position = _map.position _map_view.accept_event() elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and event.pressed: _map_origin = (_map_view.size - _map.size) * 0.5 _map.position = _map_origin _overlay.position = _map.position _map_view.accept_event() func _process(_dt: float) -> void: if is_open(): _update_player_marker() _overlay.queue_redraw() func _player() -> Node3D: return player_getter.call() if player_getter.is_valid() else null func _update_player_marker() -> void: if _player_mark == null or not is_instance_valid(_player_mark): return var p := _player() if p == null: return var world_pos: Vector3 = p.global_position var px := Vector2(world_pos.x / _world_size_m.x * _map_px.x, world_pos.z / _world_size_m.y * _map_px.y) # Marker is a child of the overlay, whose origin tracks the draggable map. _player_mark.position = px - Vector2(MARK_SIZE, MARK_SIZE) * 0.5 var server_cm := MapCoord.to_server_cm(world_pos) _coord.text = "坐标:X %d Y %d" % [roundi(server_cm.x), roundi(server_cm.y)] func _world_to_px(world_pos: Vector3) -> Vector2: return Vector2(world_pos.x / _world_size_m.x * _map_px.x, world_pos.z / _world_size_m.y * _map_px.y) func _draw_world_marks(canvas: CanvasItem) -> void: if client == null: return if client.has_method("get_party") and client.has_method("get_entity"): for member in client.get_party(): var vid := int(member.get("vid", 0)) if vid == 0: continue var entity: Dictionary = client.get_entity(vid) if entity.is_empty(): continue var party_pos := _world_to_px(MapCoord.to_world(entity.get("pos", Vector3.ZERO))) canvas.draw_circle(party_pos, 5.0, Color(0.35, 1.0, 0.55, 0.95)) if bool(member.get("leader", false)): canvas.draw_arc(party_pos, 8.0, 0.0, TAU, 20, Color(1.0, 0.9, 0.25, 0.95), 2.0) # Guild areas arrive in global server centimetres, matching GC_LAND_LIST. if client.has_method("get_land_areas"): for area in client.get_land_areas(): var x := float(area.get("x", 0)) var y := float(area.get("y", 0)) var w := float(area.get("width", 0)) var h := float(area.get("height", 0)) var p0 := _world_to_px(MapCoord.to_world(Vector3(x * 0.01, 0, -y * 0.01))) var p1 := _world_to_px(MapCoord.to_world(Vector3((x + w) * 0.01, 0, -(y + h) * 0.01))) var rect := Rect2(Vector2(min(p0.x, p1.x), min(p0.y, p1.y)), Vector2(abs(p1.x - p0.x), abs(p1.y - p0.y))) var col := Color(0.35, 0.95, 0.95, 0.8) if int(area.get("guild_id", 0)) == 0 \ else Color(1.0, 0.75, 0.2, 0.9) canvas.draw_rect(rect, col, false, 2.0) # Observer points are exposed in the same network/Godot frame as entities. if client.has_method("get_observers"): for observer in client.get_observers(): var p: Vector3 = observer.get("pos", Vector3.ZERO) var op := _world_to_px(MapCoord.to_world(p)) canvas.draw_colored_polygon(PackedVector2Array([op + Vector2(0, -6), op + Vector2(6, 0), op + Vector2(0, 6), op + Vector2(-6, 0)]), Color(0.8, 0.35, 1.0, 0.95)) # Quest/warp markers remain visible when the Atlas is used for navigation. if client.has_method("get_world_markers"): for marker in client.get_world_markers(): var mp := _world_to_px(MapCoord.to_world(marker.get("pos", Vector3.ZERO))) canvas.draw_rect(Rect2(mp - Vector2(4, 4), Vector2(8, 8)), Color(1.0, 0.55, 0.1, 0.95))