Files
mtgodot-poc/project/ui/atlas_ui.gd
T
shenleiandClaude Sonnet 5 f32064c74a 40250 classic: parallel-dev checkpoint through increment 49 (W0 interface freeze)
Captures the uncommitted parallel-development work (increments 46-49) on the
40250 classic client port, folded into docs/CLIENT-GAP.md + CLIENT-GAP-FIX.md.

Increment 49 (W0 interface-freeze batch, Phase 1 prerequisite) lands the
cross-workflow shared-file scaffolding so the 4 Phase 1 workflows can run in
isolated worktrees without contention:

- entity_store.{h,cpp}: Entity gains empire/affect_flags/owner_vid/state_flags;
  new mut_spawn_full() (single-shot two-packet merge, §2.3), mut_ownership()
  (§2.5), mut_map_bgm() + take_bgm_dirty()/bgm_name()/bgm_volume() (§9.1),
  drain_dirty() (§2.1/§2.5 bare-field-update queue); mut_char_info() now stores
  empire; reset_for_map_change() clears m_dirty.
- m2_client.cpp: new bgm_changed(name, volume) signal; classic + m2dev pump
  loops drain drain_dirty() -> entity_info and take_bgm_dirty() -> bgm_changed;
  entity_dict() exposes the 4 new keys.
- classic/classic_parser.{h,cpp}: GC_MAIN_CHARACTER3_BGM /
  GC_MAIN_CHARACTER4_BGM_VOL route bgm_name/bgm_volume to mut_map_bgm() (was
  discarded); new m_pending_actor staging map (W1 fills §2.2 merge logic).
- project/bgm_director.gd (new) + game_scene.gd: BGM consumption split out of
  net_world.gd so W2/W4 don't collide; §8.6/§4.8 keybind convergence (digits
  1-4 -> quickslots 0-3, F1-F4 -> quickslots 4-7, Ctrl+1..9 -> _emote()).

Tests: cmake --build build clean; ctest 16/16; 15 GDScript regressions green
(netbridge, gamescene, netplay, p9, p10, p2b, p8, system_menu_ui, skill,
combat_fx, skill_fx, player_motion, char_status_ui, chat, inventory), no skips.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
2026-09-02 15:13:01 +09:00

251 lines
8.7 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 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))