Files

305 lines
11 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
var _center_position_adjust := Vector2.ZERO
var _touches: Dictionary = {}
var _touch_dragging := false
var _touch_drag_origin := Vector2.ZERO
var _touch_map_origin := Vector2.ZERO
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
_touches.clear()
_touch_dragging = false
# CPythonMiniMap::SetAtlasCenterPosition ultimately calls the atlas window's
# SetCenterPositionAdjust. Keep the same screen-space adjustment instead of
# treating this event as a world-coordinate teleport.
func set_center_position_adjust(x: int, y: int) -> void:
_center_position_adjust = Vector2(x, y)
if _map_view and _map:
_map_origin = (_map_view.size - _map.size) * 0.5 + _center_position_adjust
_map.position = _map_origin
_overlay.position = _map.position
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.mouse_filter = Control.MOUSE_FILTER_STOP
_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 + _center_position_adjust
_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 InputEventScreenTouch:
var touch := event as InputEventScreenTouch
if touch.pressed:
_touches[touch.index] = touch.position
if _touches.size() == 1:
_touch_dragging = true
_touch_drag_origin = touch.position
_touch_map_origin = _map.position
else:
# A second finger is a gesture boundary. Do not continue the
# first finger's drag while pinch/scroll state is ambiguous.
_touch_dragging = false
else:
_touches.erase(touch.index)
if _touches.is_empty():
_touch_dragging = false
elif _touches.size() == 1:
# Re-arm panning for the remaining finger without jumping the map.
var remaining := int(_touches.keys()[0])
_touch_dragging = true
_touch_drag_origin = _touches[remaining]
_touch_map_origin = _map.position
_map_view.accept_event()
return
if event is InputEventScreenDrag:
var drag := event as InputEventScreenDrag
if _touches.has(drag.index):
_touches[drag.index] = drag.position
if _touches.size() == 1 and _touch_dragging and _touches.has(drag.index):
_map.position = _touch_map_origin + drag.position - _touch_drag_origin
_overlay.position = _map.position
_map_view.accept_event()
return
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:
_recenter_map()
_map_view.accept_event()
func _recenter_map() -> void:
_map_origin = (_map_view.size - _map.size) * 0.5 + _center_position_adjust
_map.position = _map_origin
_overlay.position = _map.position
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))