# ChatUI (P3) —— 聊天窗:多标签 + 输入行 + 频道前缀 + 系统/战斗日志。 # # var chat := preload("res://ui/chat_ui.gd").new() # add_child(chat) # chat.setup(m2client, canvas_parent) # canvas_parent: 一个 CanvasLayer/Control # chat.focus_input() # Enter 键调 # # 输入行前缀: /w <名> <话> 私聊 · /g 公会 · /p 队伍 · /s 喊话 · 其它 普通。 # 消费 M2Client:chat / whisper / item_picked_up。按类型分到 全部 / 私聊 / 系统 / 战斗 标签。 extends Node # EChatType (wire.h) const T_TALKING := 0 const T_INFO := 1 const T_NOTICE := 2 const T_PARTY := 3 const T_GUILD := 4 const T_SHOUT := 6 const T_WHISPER := 7 const FISHING_SUCCESS := 3 const FISHING_FAIL := 4 const FISHING_FISH := 5 const TABS := ["全部", "私聊", "系统", "战斗"] const COLOR := { 0: Color(0.92, 0.92, 0.92), # talking 1: Color(0.55, 0.85, 1.0), # info 2: Color(1.0, 0.85, 0.3), # notice 3: Color(0.6, 0.85, 1.0), # party 4: Color(0.5, 1.0, 0.6), # guild 6: Color(1.0, 0.7, 0.4), # shout 7: Color(1.0, 0.6, 0.95), # whisper } signal line_added(tab: int, bbcode: String) var client: Node var _root: Control var _log := {} # tab_index -> RichTextLabel var _input: LineEdit var _tab := 0 var _tab_btns := [] var _max_lines := 200 var _buffers := {} # tab -> Array[String] func setup(m2client: Node, parent: Node) -> void: client = m2client _build(parent) if client: if client.has_signal("chat"): client.chat.connect(_on_chat) if client.has_signal("whisper_received"): client.whisper_received.connect(_on_whisper) if client.has_signal("item_picked_up"): client.item_picked_up.connect(_on_pickup) if client.has_signal("fishing_event"): client.fishing_event.connect(_on_fishing_event) func focus_input() -> void: if _input: _input.grab_focus() # 预填 "/w <名> " 并聚焦(好友列表点名字时调)。 func start_whisper(name: String) -> void: if _input: _input.text = "/w %s " % name _input.caret_column = _input.text.length() _input.grab_focus() func is_typing() -> bool: return _input != null and _input.has_focus() # --- 建 UI -------------------------------------------------------------- func _build(parent: Node) -> void: _root = Control.new() _root.set_anchors_preset(Control.PRESET_BOTTOM_LEFT) _root.position = Vector2(12, -232) _root.size = Vector2(460, 220) 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.05, 0.06, 0.08, 0.72) sb.set_corner_radius_all(4) panel.add_theme_stylebox_override("panel", sb) _root.add_child(panel) var tabrow := HBoxContainer.new() tabrow.position = Vector2(6, 4) _root.add_child(tabrow) for i in TABS.size(): var b := Button.new() b.text = TABS[i] b.toggle_mode = true b.button_pressed = (i == 0) b.add_theme_font_size_override("font_size", 11) var idx := i b.pressed.connect(func(): _select_tab(idx)) tabrow.add_child(b) _tab_btns.append(b) for i in TABS.size(): var rt := RichTextLabel.new() rt.bbcode_enabled = true rt.scroll_following = true rt.selection_enabled = true rt.position = Vector2(6, 30) rt.size = Vector2(448, 158) rt.visible = (i == 0) rt.add_theme_font_size_override("normal_font_size", 12) _root.add_child(rt) _log[i] = rt _buffers[i] = [] _input = LineEdit.new() _input.placeholder_text = "回车发送 · /w 名 私聊 · /g 公会 · /p 队伍 · /s 喊话" _input.position = Vector2(6, 192) _input.size = Vector2(448, 24) _input.add_theme_font_size_override("font_size", 12) _input.text_submitted.connect(_on_submit) _root.add_child(_input) func _select_tab(i: int) -> void: _tab = i for t in _log: _log[t].visible = (t == i) for k in _tab_btns.size(): _tab_btns[k].button_pressed = (k == i) # --- 输入 ------------------------------------------------------------- func _on_submit(text: String) -> void: _input.clear() if _input.is_inside_tree(): _input.release_focus() var s := text.strip_edges() if s == "" or client == null: return if s.begins_with("/w ") or s.begins_with("/whisper "): var rest := s.substr(s.find(" ") + 1).strip_edges() var sp := rest.find(" ") if sp > 0: var to := rest.substr(0, sp) var msg := rest.substr(sp + 1).strip_edges() client.whisper(to, msg) _append(1, T_WHISPER, "→ %s: %s" % [to, msg]) return var map := {"/g ": T_GUILD, "/p ": T_PARTY, "/s ": T_SHOUT} for pre in map: if s.begins_with(pre): client.say(map[pre], s.substr(3).strip_edges()) return client.say(T_TALKING, s) # --- 收消息 -------------------------------------------------------------- func _on_chat(type: int, vid: int, text: String) -> void: var who := "" if client and client.has_method("get_entity"): var e: Dictionary = client.get_entity(vid) if not e.is_empty(): who = String(e.get("name", "")) var body := ("%s: %s" % [who, text]) if who != "" else text var tab := 0 if type == T_INFO or type == T_NOTICE: tab = 2 _append(tab, type, body) if tab != 0: _append(0, type, body) # 全部标签也收一份 func _on_whisper(sub: int, from: String, text: String) -> void: var tag := from if sub == 1: tag = "[系统]" elif sub == 2: tag = "[GM] " + from _append(1, T_WHISPER, "%s: %s" % [tag, text]) _append(0, T_WHISPER, "%s: %s" % [tag, text]) func _on_pickup(vnum: int, count: int, from: String) -> void: var s := "拾取 #%d x%d" % [vnum, count] if from != "": s += "(来自 %s)" % from _append(2, T_INFO, s) _append(3, T_INFO, s) func _on_fishing_event(subheader: int, info: int, _dir: int) -> void: var s := "" if subheader == FISHING_SUCCESS: s = "鱼钩命中" elif subheader == FISHING_FAIL: s = "钓鱼失败" elif subheader == FISHING_FISH: s = "捕获物品 #%d" % info if info != 0 else "没有捕获到物品" if s != "": _append(2, T_INFO, s) _append(3, T_INFO, s) # --- 追加行 -------------------------------------------------------------- func _append(tab: int, type: int, body: String) -> void: var c: Color = COLOR.get(type, COLOR[0]) var line := "[color=#%s]%s[/color]" % [c.to_html(false), body] var buf: Array = _buffers[tab] buf.append(line) if buf.size() > _max_lines: buf.pop_front() var rt: RichTextLabel = _log[tab] rt.text = "\n".join(buf) line_added.emit(tab, line)