Files
mtgodot-poc/project/ui/atlas_ui.gd
T
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
2026-08-31 20:02:12 +09:00

243 lines
8.6 KiB
GDScript
Raw 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)
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 _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))