Files
mtgodot-poc/project/ui/friend_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

144 lines
4.4 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.
# FriendUI (P8) —— 好友 / 私聊名单窗(M 键)。
#
# var fu := preload("res://ui/friend_ui.gd").new()
# add_child(fu)
# fu.setup(m2client, canvas_parent)
# fu.toggle() # M 键
#
# 每行 = 在线点 + 名字 + [删除]。底部输入框 + [添加好友]。
# `friends_changed` 刷新。双击某行 → emit `whisper_to(name)`chat_ui 可接)。
extends Node
signal whisper_to(name: String)
var client: Node
var _root: Control
var _list: VBoxContainer
var _name_edit: LineEdit
var _invite_dialog: ConfirmationDialog
var _pending_invite := ""
func setup(m2client: Node, parent: Node) -> void:
client = m2client
_build(parent)
if client.has_signal("friends_changed"):
client.friends_changed.connect(refresh)
if client.has_signal("friend_invite_ask"):
client.friend_invite_ask.connect(_on_friend_invite)
func is_open() -> bool:
return _root != null and _root.visible
func close() -> void:
if _root:
_root.visible = false
func toggle() -> void:
_root.visible = not _root.visible
if _root.visible:
refresh()
func refresh() -> void:
if not is_open() or client == null:
return
for c in _list.get_children():
c.queue_free()
var friends: Array = client.get_friends()
if friends.is_empty():
var e := Label.new()
e.text = "(好友列表为空)"
e.add_theme_font_size_override("font_size", 11)
_list.add_child(e)
return
for f in friends:
_list.add_child(_row(f))
func _row(f: Dictionary) -> Control:
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(260, 0)
var dot := Label.new()
dot.text = "●"
dot.modulate = Color(0.4, 0.9, 0.4) if f.get("online", false) else Color(0.4, 0.4, 0.4)
row.add_child(dot)
var nm := Button.new()
nm.text = String(f.get("name", "?"))
nm.flat = true
nm.custom_minimum_size = Vector2(150, 0)
nm.pressed.connect(func() -> void: whisper_to.emit(String(f.get("name", ""))))
row.add_child(nm)
if f.get("mobile", false):
var mobile := Label.new()
mobile.text = "手机"
mobile.modulate = Color(0.45, 0.8, 1.0)
mobile.add_theme_font_size_override("font_size", 10)
row.add_child(mobile)
var del := Button.new()
del.text = "×"
del.pressed.connect(func() -> void: client.remove_friend(String(f.get("name", ""))))
row.add_child(del)
return row
func _on_add() -> void:
var nm := _name_edit.text.strip_edges()
if nm != "":
client.add_friend(nm)
_name_edit.clear()
func _on_friend_invite(name: String) -> void:
if client == null or name.strip_edges() == "":
return
_pending_invite = name
_invite_dialog.dialog_text = "接受“%s”的好友请求?" % name
_invite_dialog.popup_centered()
func _answer_invite(accept: bool) -> void:
var name := _pending_invite
_pending_invite = ""
if name != "" and client != null and client.has_method("friend_answer"):
client.friend_answer(name, accept)
func _build(parent: Node) -> void:
_root = Control.new()
_root.set_anchors_preset(Control.PRESET_CENTER)
_root.position = Vector2(-150, -180)
_root.size = Vector2(300, 360)
_root.visible = false
_root.set_meta("is_titlebar", true)
parent.add_child(_root)
var panel := Panel.new()
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.07, 0.08, 0.1, 0.96)
sb.set_corner_radius_all(4)
panel.add_theme_stylebox_override("panel", sb)
_root.add_child(panel)
var title := Label.new()
title.text = "好友"
title.position = Vector2(12, 8)
_root.add_child(title)
_list = VBoxContainer.new()
_list.position = Vector2(12, 34)
_list.add_theme_constant_override("separation", 3)
_root.add_child(_list)
var bottom := HBoxContainer.new()
bottom.position = Vector2(12, 320)
bottom.custom_minimum_size = Vector2(276, 0)
_root.add_child(bottom)
_name_edit = LineEdit.new()
_name_edit.placeholder_text = "角色名"
_name_edit.custom_minimum_size = Vector2(190, 0)
bottom.add_child(_name_edit)
var add_btn := Button.new()
add_btn.text = "添加"
add_btn.pressed.connect(_on_add)
bottom.add_child(add_btn)
_invite_dialog = ConfirmationDialog.new()
_invite_dialog.title = "好友请求"
_invite_dialog.ok_button_text = "接受"
_invite_dialog.cancel_button_text = "拒绝"
_invite_dialog.confirmed.connect(func() -> void: _answer_invite(true))
_invite_dialog.canceled.connect(func() -> void: _answer_invite(false))
# Keep the dialog outside the toggleable friend panel: requests must still
# be visible when the friend list window itself is closed.
parent.add_child(_invite_dialog)