# Minimap (P9) —— 右上角圆形小地图。北朝上。 # # var mm := preload("res://ui/minimap.gd").new() # add_child(mm) # mm.setup(m2client, canvas_parent, func() -> Node3D: return player) # # 画:底圆 + 外环 + 玩家箭头 + 实体蓝点 + 怪红点 + NPC 黄点 + 传送青点 + 任务标记橙星 + # 观战者紫色菱形 + 公会领地边框;支持 SetScale / 鼠标滚轮缩放。 # 世界→图:Δ = blip.xz - player.xz,按 scale 转像素,(+x 右, -z 上),超出半径夹到边缘。 extends Node const RADIUS := 70.0 const SCALE := 0.25 # 像素/米(越大越放大) const NPC_WARP_TYPE := 2 # NPCMark.type: 2 = 传送 const MIN_SCALE := 0.10 const MAX_SCALE := 1.25 var client: Node var player_getter: Callable var _root: Control var _view: Control var _ch_label: Label var _scale := SCALE func setup(m2client: Node, parent: Node, get_player: Callable) -> void: client = m2client player_getter = get_player _build(parent) if client and client.has_signal("channel_changed"): client.channel_changed.connect(func(c): _ch_label.text = "CH %d" % c) func _build(parent: Node) -> void: _root = Control.new() _root.set_anchors_preset(Control.PRESET_TOP_RIGHT) _root.position = Vector2(-2 * RADIUS - 16, 12) _root.size = Vector2(2 * RADIUS, 2 * RADIUS + 16) parent.add_child(_root) _view = Control.new() _view.size = Vector2(2 * RADIUS, 2 * RADIUS) _view.mouse_filter = Control.MOUSE_FILTER_IGNORE _view.draw.connect(_draw_map) _root.add_child(_view) _root.gui_input.connect(_on_gui_input) _root.mouse_filter = Control.MOUSE_FILTER_STOP _ch_label = Label.new() _ch_label.position = Vector2(0, 2 * RADIUS) _ch_label.add_theme_font_size_override("font_size", 11) _ch_label.text = "CH -" _root.add_child(_ch_label) func _process(_dt: float) -> void: if _view: _view.queue_redraw() # Matches CPythonMiniMap::SetScale: callers may provide pixels per world metre. # The wheel gesture is intentionally local to the minimap so world controls do # not change when the pointer is elsewhere. func set_scale(pixels_per_meter: float) -> void: _scale = clampf(pixels_per_meter, MIN_SCALE, MAX_SCALE) if _view: _view.queue_redraw() func get_scale() -> float: return _scale func _on_gui_input(event: InputEvent) -> void: if event is InputEventMouseButton and event.pressed: if event.button_index == MOUSE_BUTTON_WHEEL_UP: set_scale(_scale * 1.15) _root.accept_event() elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN: set_scale(_scale / 1.15) _root.accept_event() func _player() -> Node3D: return player_getter.call() if player_getter.is_valid() else null func _to_map(world: Vector3, origin: Vector3) -> Vector2: var d := world - origin # screen: +x right, +y down. world −z = north -> screen up (−y). world +x = east -> right. var p := Vector2(d.x, d.z) * _scale if p.length() > RADIUS - 4.0: p = p.normalized() * (RADIUS - 4.0) return Vector2(RADIUS, RADIUS) + p func _draw_map() -> void: var c := Vector2(RADIUS, RADIUS) _view.draw_circle(c, RADIUS, Color(0.05, 0.06, 0.08, 0.82)) _view.draw_arc(c, RADIUS - 1.5, 0, TAU, 48, Color(0.6, 0.6, 0.7, 0.7), 2.0) var pl := _player() if pl == null or client == null: return var origin: Vector3 = pl.global_position var party_vids := {} if client.has_method("get_party"): for member in client.get_party(): var pvid := int(member.get("vid", 0)) if pvid != 0: party_vids[pvid] = bool(member.get("leader", false)) # 实体:怪红、其它蓝 if client.has_method("get_entities"): for e in client.get_entities(): if bool(e.get("is_main", false)): continue var pos: Vector3 = e.get("pos", Vector3.ZERO) var vid := int(e.get("vid", 0)) var map_pos := _to_map(MapCoord.to_world(pos), origin) var col := Color(0.35, 1.0, 0.55) if party_vids.has(vid) \ else Color(0.4, 0.6, 1.0) if int(e.get("ch_type", 0)) != 2 else Color(1.0, 0.35, 0.3) _view.draw_circle(map_pos, 2.5, col) if bool(party_vids.get(vid, false)): _view.draw_arc(map_pos, 4.5, 0.0, TAU, 16, Color(1.0, 0.9, 0.25, 0.9), 1.0) # NPC 标记:传送青、其它黄 if client.has_method("get_npc_marks"): for m in client.get_npc_marks(): var col := Color(0.3, 0.9, 0.9) if int(m.get("type", 0)) == NPC_WARP_TYPE else Color(1.0, 0.85, 0.2) _view.draw_circle(_to_map(MapCoord.to_world(m.get("pos", Vector3.ZERO)), origin), 3.0, col) # 观战者:原版在小地图上显示独立的观察点(紫色菱形)。 if client.has_method("get_observers"): for o in client.get_observers(): var op := _to_map(MapCoord.to_world(o.get("pos", Vector3.ZERO)), origin) var diamond := PackedVector2Array([op + Vector2(0, -4), op + Vector2(4, 0), op + Vector2(0, 4), op + Vector2(-4, 0)]) _view.draw_colored_polygon(diamond, Color(0.8, 0.35, 1.0, 0.95)) # 公会领地:GC_LAND_LIST 的全局厘米矩形,在小地图上画细边框; # guild_id=0 使用中性青色,其余领地使用金色。 if client.has_method("get_land_areas"): for a in client.get_land_areas(): var x := float(a.get("x", 0)) var y := float(a.get("y", 0)) var w := float(a.get("width", 0)) var h := float(a.get("height", 0)) var p0 := _to_map(MapCoord.to_world(Vector3(x * 0.01, 0, -y * 0.01)), origin) var p1 := _to_map(MapCoord.to_world(Vector3((x + w) * 0.01, 0, -(y + h) * 0.01)), origin) 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.3, 0.9, 0.9, 0.75) if int(a.get("guild_id", 0)) == 0 \ else Color(1.0, 0.75, 0.2, 0.85) _view.draw_rect(rect, col, false, 1.0) # 任务标记:橙星(小方块代替) if client.has_method("get_world_markers"): for m in client.get_world_markers(): var mp := _to_map(MapCoord.to_world(m.get("pos", Vector3.ZERO)), origin) _view.draw_rect(Rect2(mp - Vector2(3, 3), Vector2(6, 6)), Color(1.0, 0.6, 0.1)) # 玩家箭头(朝向) var yaw: float = pl.rotation.y var fwd := Vector2(sin(yaw), cos(yaw)) # world +? -> map;与 _to_map 的 (x,-z) 对齐 var tip := c + fwd * 8.0 var left := c + fwd.rotated(deg_to_rad(140)) * 6.0 var right := c + fwd.rotated(deg_to_rad(-140)) * 6.0 _view.draw_colored_polygon(PackedVector2Array([tip, left, right]), Color(1, 1, 1))