Files
mtgodot-poc/project/ui/guild_ui.gd
T

537 lines
19 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.
# GuildUI (P8/M4) —— 公会窗(G 键):成员 / 技能 / 公会战 / 公告 四页。
#
# var gu := preload("res://ui/guild_ui.gd").new()
# add_child(gu)
# gu.setup(m2client, canvas_parent, skill_table) # skill_table 可空(出公会技能名)
# gu.toggle() # G 键
#
# `guild_changed` / `guild_skill_changed` / `guild_war_changed` 刷新,`guild_marks_ready`
# 到时刷会徽图。等级/日志页仍取决于协议是否提供对应数据,不在当前四页入口内伪造。
extends Node
const TABS := ["成员", "技能", "公会战", "公告"]
# EGuildWarState (wire.h)
const WAR_STATE_NAMES := ["无", "已宣战", "被拒", "收到宣战", "待开始", "取消", "交战中", "结束"]
var client: Node
var skill_table: Object # SkillTable (tests may provide a Node stub)
var _ui_parent: Node
# 返回一张 Image(16×12,或任意会被转换/缩放)用作会徽上传源;null = 不显示上传按钮。
var mark_image_provider: Callable
var _root: Control
var _title: Label
var _info: Label
var _mark: TextureRect
var _tabrow: HBoxContainer
var _scroll: ScrollContainer
var _stack: Control
var _tab := 0
var _tab_btns: Array[Button] = []
var _pages: Array[VBoxContainer] = []
var _war_name_edit: LineEdit
var _upload_status: Label
var _create_dialog: ConfirmationDialog
var _create_name_edit: LineEdit
var _create_status: Label
var _mobile_mode := false
const MOBILE_SIZE := Vector2(700, 340)
func setup(m2client: Node, parent: Node, table: Object = null) -> void:
client = m2client
skill_table = table
_ui_parent = parent
_build(parent)
for sig in ["guild_changed", "guild_skill_changed", "guild_war_changed", "guild_comments_changed"]:
if client.has_signal(sig):
client.connect(sig, refresh)
if client.has_signal("guild_make_requested"):
client.guild_make_requested.connect(_on_guild_make_requested)
if client.has_signal("guild_marks_ready"):
client.guild_marks_ready.connect(func(_n): refresh())
if client.has_signal("guild_mark_uploaded"):
client.guild_mark_uploaded.connect(_on_mark_uploaded)
func is_open() -> bool:
return _root != null and _root.visible
func set_mobile_mode(enabled: bool) -> void:
_mobile_mode = enabled
_apply_mobile_layout()
if _mobile_mode and is_open() and _ui_parent and _ui_parent.has_method("reflow_mobile_window"):
_ui_parent.reflow_mobile_window(_root)
func close() -> void:
if _root:
_root.visible = false
func get_mobile_window() -> Control:
return _root
func toggle() -> void:
_root.visible = not _root.visible
if _root.visible:
refresh()
func _set_tab(i: int) -> void:
_tab = i
for t in _pages.size():
_pages[t].visible = (t == i)
_tab_btns[t].disabled = (t == i)
refresh()
func refresh() -> void:
if not is_open() or client == null:
return
var g: Dictionary = client.get_guild()
var in_guild: bool = g.get("in_guild", false)
_refresh_mark(int(g.get("id", 0)) if in_guild else 0)
if not in_guild:
_info.text = "(未加入公会)"
for p in _pages:
for c in p.get_children():
c.queue_free()
return
_info.text = "%s Lv.%d 成员 %d/%d 资金 %d" % [
g.get("name", "?"), int(g.get("level", 0)),
int(g.get("member_count", 0)), int(g.get("max_member_count", 0)),
int(g.get("gold", 0))]
match _tab:
0: _fill_members()
1: _fill_skills()
2: _fill_war(int(g.get("id", 0)))
3: _fill_comments()
func _fill_members() -> void:
var page := _pages[0]
for c in page.get_children():
c.queue_free()
# 会徽上传(需要配了 mark server + 提供了图源)
var srv: Dictionary = client.get_mark_server() if client.has_method("get_mark_server") else {}
if mark_image_provider.is_valid() and int(srv.get("port", 0)) > 0:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
page.add_child(row)
var ub := Button.new()
ub.text = "上传会徽"
ub.custom_minimum_size = Vector2(140, 40) if _mobile_mode else Vector2(96, 26)
ub.pressed.connect(_upload_mark)
row.add_child(ub)
_upload_status = Label.new()
_upload_status.add_theme_font_size_override("font_size", 11)
_upload_status.modulate = Color(0.7, 0.8, 0.7)
row.add_child(_upload_status)
var grades: Array = client.get_guild_grades()
var members: Array = client.get_guild_members()
members.sort_custom(func(a, b): return int(a.get("grade", 99)) < int(b.get("grade", 99)))
for m in members:
var gi := int(m.get("grade", 0))
var gname := ""
if gi >= 1 and gi <= grades.size():
gname = String(grades[gi - 1].get("name", ""))
var row := Label.new()
row.custom_minimum_size = Vector2(640, 34) if _mobile_mode else Vector2.ZERO
row.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
row.add_theme_font_size_override("font_size", 14 if _mobile_mode else 12)
row.text = "%s Lv.%d [%s] 贡献 %d%s" % [
String(m.get("name", "?")), int(m.get("level", 0)),
gname if gname != "" else str(gi),
int(m.get("offer", 0)),
(" 在线" if m.get("online", false) else " 离线") + (" ★" if m.get("general", false) else "")]
page.add_child(row)
func _fill_comments() -> void:
var page := _pages[3]
for c in page.get_children():
c.queue_free()
if not client.has_method("get_guild_comments"):
var missing := Label.new()
missing.text = "(当前协议没有公告接口)"
page.add_child(missing)
return
var actions := HBoxContainer.new()
actions.add_theme_constant_override("separation", 5)
page.add_child(actions)
var edit := LineEdit.new()
edit.placeholder_text = "公告内容(最多 50 字)"
edit.custom_minimum_size = Vector2(440, 40) if _mobile_mode else Vector2(245, 26)
actions.add_child(edit)
var post := Button.new()
post.text = "发布"
post.custom_minimum_size = Vector2(92, 40) if _mobile_mode else Vector2(52, 26)
post.pressed.connect(func():
var text := edit.text.strip_edges()
if text != "" and client.has_method("guild_post_comment"):
client.guild_post_comment(text))
actions.add_child(post)
var refresh_btn := Button.new()
refresh_btn.text = "刷新"
refresh_btn.custom_minimum_size = Vector2(92, 40) if _mobile_mode else Vector2(52, 26)
refresh_btn.pressed.connect(func():
if client.has_method("guild_refresh_comments"):
client.guild_refresh_comments())
actions.add_child(refresh_btn)
var comments: Array = client.get_guild_comments()
if comments.is_empty():
var none := Label.new()
none.text = "(暂无公告)"
page.add_child(none)
return
for item in comments:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 6)
page.add_child(row)
var label := Label.new()
label.custom_minimum_size = Vector2(560, 42) if _mobile_mode else Vector2(286, 0)
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", 13 if _mobile_mode else 12)
label.text = "%s%s" % [String(item.get("name", "?")), String(item.get("content", ""))]
row.add_child(label)
var delete := Button.new()
delete.text = "删"
delete.custom_minimum_size = Vector2(64, 40) if _mobile_mode else Vector2(32, 24)
delete.pressed.connect(func():
if client.has_method("guild_delete_comment"):
client.guild_delete_comment(int(item.get("id", 0))))
row.add_child(delete)
func _guild_skill_rows() -> Array:
# skill_table 的 GUILD 分类给 id(升序);按顺序配 levels[i]。返回 [{id, name}]。
var out := []
if skill_table and skill_table.has_method("for_category"):
for id in skill_table.for_category("GUILD"):
out.append({"id": int(id), "name": skill_table.name_of(int(id))})
return out
func _fill_skills() -> void:
var page := _pages[1]
for c in page.get_children():
c.queue_free()
var sk: Dictionary = client.get_guild_skill()
if not sk.get("valid", false):
var l := Label.new()
l.text = "(还没收到公会技能数据)"
page.add_child(l)
return
var head := Label.new()
head.custom_minimum_size = Vector2(640, 34) if _mobile_mode else Vector2.ZERO
head.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
head.add_theme_font_size_override("font_size", 14 if _mobile_mode else 12)
head.modulate = Color(0.8, 0.9, 1.0)
head.text = "技能点 %d 公会点 %d / %d" % [
int(sk.get("skill_point", 0)), int(sk.get("guild_point", 0)), int(sk.get("max_guild_point", 0))]
page.add_child(head)
var levels: Array = sk.get("levels", [])
var meta := _guild_skill_rows()
for i in levels.size():
var lv := int(levels[i])
var nm := "公会技能 %d" % i
var sid := -1
if i < meta.size():
nm = String(meta[i].get("name", nm))
sid = int(meta[i].get("id", -1))
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
page.add_child(row)
var lbl := Label.new()
lbl.add_theme_font_size_override("font_size", 14 if _mobile_mode else 12)
lbl.custom_minimum_size = Vector2(430, 40) if _mobile_mode else Vector2(240, 0)
lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
lbl.text = "%s Lv %d" % [nm, lv]
row.add_child(lbl)
if sid >= 0 and lv > 0:
var btn := Button.new()
btn.text = "施放"
btn.custom_minimum_size = Vector2(92, 40) if _mobile_mode else Vector2(56, 24)
btn.pressed.connect(func(): client.use_guild_skill(sid, 0))
row.add_child(btn)
func _fill_war(my_guild_id: int) -> void:
var page := _pages[2]
for c in page.get_children():
c.queue_free()
var cur: Dictionary = client.get_guild_war()
var st := int(cur.get("state", 0))
var cur_lbl := Label.new()
cur_lbl.custom_minimum_size = Vector2(640, 36) if _mobile_mode else Vector2.ZERO
cur_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
cur_lbl.add_theme_font_size_override("font_size", 14 if _mobile_mode else 12)
cur_lbl.modulate = Color(1.0, 0.8, 0.7)
if st == 0:
cur_lbl.text = "当前无公会战"
else:
var on := String(cur.get("opp_name", ""))
cur_lbl.text = "对 %s %s" % [
on if on != "" else "#%d" % int(cur.get("opp_guild_id", 0)),
WAR_STATE_NAMES[st] if st < WAR_STATE_NAMES.size() else str(st)]
page.add_child(cur_lbl)
# 宣战输入
var decl := HBoxContainer.new()
decl.add_theme_constant_override("separation", 6)
page.add_child(decl)
_war_name_edit = LineEdit.new()
_war_name_edit.placeholder_text = "对方公会名"
_war_name_edit.custom_minimum_size = Vector2(440, 40) if _mobile_mode else Vector2(200, 26)
decl.add_child(_war_name_edit)
var db := Button.new()
db.text = "宣战"
db.custom_minimum_size = Vector2(92, 40) if _mobile_mode else Vector2(60, 26)
db.pressed.connect(func():
var nm := _war_name_edit.text.strip_edges()
if nm != "":
client.declare_guild_war(nm))
decl.add_child(db)
var hdr := Label.new()
hdr.custom_minimum_size = Vector2(640, 34) if _mobile_mode else Vector2.ZERO
hdr.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
hdr.add_theme_font_size_override("font_size", 14 if _mobile_mode else 12)
hdr.text = "— 进行中的公会战 —"
page.add_child(hdr)
var wars: Array = client.get_guild_wars()
if wars.is_empty():
var none := Label.new()
none.text = "(无)"
page.add_child(none)
for wpair in wars:
var sn := String(wpair.get("src_name", ""))
var dn := String(wpair.get("dst_name", ""))
var row := Label.new()
row.custom_minimum_size = Vector2(640, 34) if _mobile_mode else Vector2.ZERO
row.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
row.add_theme_font_size_override("font_size", 14 if _mobile_mode else 12)
var mark_self := " ◀我方" if int(wpair.get("src", 0)) == my_guild_id or int(wpair.get("dst", 0)) == my_guild_id else ""
row.text = "%s vs %s%s" % [
sn if sn != "" else "#%d" % int(wpair.get("src", 0)),
dn if dn != "" else "#%d" % int(wpair.get("dst", 0)),
mark_self]
page.add_child(row)
func _upload_mark() -> void:
var g: Dictionary = client.get_guild()
if not g.get("in_guild", false) or not mark_image_provider.is_valid():
return
var img = mark_image_provider.call()
if img == null:
if _upload_status:
_upload_status.text = "没有会徽图"
return
var srv: Dictionary = client.get_mark_server()
var ok: bool = client.upload_guild_mark(
String(srv.get("host", "")), int(srv.get("port", 0)), int(g.get("id", 0)), img)
if _upload_status:
_upload_status.text = "上传中…" if ok else "上传发起失败"
func _on_mark_uploaded(ok: bool) -> void:
if _upload_status:
_upload_status.text = "✔ 已上传(等服务器刷新)" if ok else "✘ 上传失败"
# 40250 原版在收到 GC_REQUEST_MAKE_GUILD 后弹 InputDialog,再由玩家回送
# CG_ANSWER_MAKE_GUILD。这里保留同样的“服务端请求 -> 输入 -> 应答”边界,
# 让名称校验发生在发包前,同时把最终权限 / 敏感词判断留给服务端。
func _on_guild_make_requested() -> void:
if is_instance_valid(_create_dialog):
_popup_create_dialog()
_create_name_edit.grab_focus()
return
if _ui_parent == null or client == null:
return
_create_dialog = ConfirmationDialog.new()
_create_dialog.name = "GuildCreateDialog"
_create_dialog.title = "创建公会"
_create_dialog.dialog_text = "请输入公会名称(最多 12 字)"
_create_dialog.ok_button_text = "创建"
_create_dialog.cancel_button_text = "取消"
_create_name_edit = LineEdit.new()
_create_name_edit.name = "GuildName"
_create_name_edit.max_length = 12
_create_name_edit.placeholder_text = "公会名称"
_create_name_edit.custom_minimum_size = Vector2(380, 44) if _mobile_mode else Vector2(250, 28)
_create_dialog.add_child(_create_name_edit)
_create_status = Label.new()
_create_status.name = "Status"
_create_status.add_theme_font_size_override("font_size", 13 if _mobile_mode else 11)
_create_status.modulate = Color(1.0, 0.7, 0.6)
_create_dialog.add_child(_create_status)
_ui_parent.add_child(_create_dialog)
_create_dialog.confirmed.connect(_on_guild_create_confirmed)
_create_dialog.canceled.connect(_close_guild_create_dialog)
if _ui_parent.has_method("track_mobile_modal"):
_ui_parent.track_mobile_modal(_create_dialog)
_popup_create_dialog()
_create_name_edit.grab_focus()
func _on_guild_create_confirmed() -> void:
if not is_instance_valid(_create_dialog) or _create_name_edit == null:
return
var name := _create_name_edit.text.strip_edges()
var error := ""
if name.is_empty():
error = "公会名称不能为空"
elif name.length() > 12:
error = "公会名称最多 12 字"
else:
for ch in name:
if ch.unicode_at(0) < 32:
error = "公会名称包含非法字符"
break
if error != "":
_create_status.text = error
_create_dialog.call_deferred("popup_centered", Vector2i(520, 270) if _mobile_mode else Vector2i(-1, -1))
_create_name_edit.call_deferred("grab_focus")
return
if not client.has_method("guild_answer_make") or not bool(client.call("guild_answer_make", name)):
_create_status.text = "创建请求发送失败"
_create_dialog.call_deferred("popup_centered", Vector2i(520, 270) if _mobile_mode else Vector2i(-1, -1))
_create_name_edit.call_deferred("grab_focus")
return
_close_guild_create_dialog()
func _close_guild_create_dialog() -> void:
var dialog := _create_dialog
_create_dialog = null
_create_name_edit = null
_create_status = null
if is_instance_valid(dialog):
dialog.queue_free()
func _popup_create_dialog() -> void:
if not is_instance_valid(_create_dialog):
return
if _mobile_mode:
_create_dialog.set_meta("mobile_title", "创建公会")
_create_dialog.min_size = Vector2i(520, 270)
_create_dialog.size = Vector2i(520, 270)
_create_dialog.popup_centered(Vector2i(520, 270))
else:
_create_dialog.popup_centered()
# 公会 id 变了就重新取会徽图(16x12,放大 3x 显示)。
func _refresh_mark(guild_id: int) -> void:
if _mark == null:
return
if guild_id == 0 or client == null or not client.has_method("get_guild_mark_image"):
_mark.texture = null
_mark.visible = false
return
var img: Image = client.get_guild_mark_image(guild_id)
if img == null:
_mark.texture = null
_mark.visible = false
return
_mark.texture = ImageTexture.create_from_image(img)
_mark.visible = true
func _build(parent: Node) -> void:
_root = Control.new()
_root.set_anchors_preset(Control.PRESET_CENTER)
_root.position = Vector2(-190, -210)
_root.size = Vector2(380, 420)
_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.97)
sb.set_corner_radius_all(4)
panel.add_theme_stylebox_override("panel", sb)
_root.add_child(panel)
_title = Label.new()
_title.text = "公会"
_title.position = Vector2(12, 8)
_root.add_child(_title)
_mark = TextureRect.new()
_mark.position = Vector2(320, 6)
_mark.custom_minimum_size = Vector2(48, 36) # 16x12 放大 3x
_mark.size = Vector2(48, 36)
_mark.stretch_mode = TextureRect.STRETCH_SCALE
_mark.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
_mark.visible = false
_root.add_child(_mark)
_info = Label.new()
_info.position = Vector2(12, 32)
_info.add_theme_font_size_override("font_size", 12)
_info.modulate = Color(0.9, 0.85, 0.6)
_root.add_child(_info)
_tabrow = HBoxContainer.new()
_tabrow.position = Vector2(12, 54)
_tabrow.add_theme_constant_override("separation", 4)
_root.add_child(_tabrow)
for i in TABS.size():
var b := Button.new()
b.text = TABS[i]
b.custom_minimum_size = Vector2(84, 26)
b.pressed.connect(_set_tab.bind(i))
_tabrow.add_child(b)
_tab_btns.append(b)
_scroll = ScrollContainer.new()
_scroll.position = Vector2(12, 86)
_scroll.custom_minimum_size = Vector2(356, 322)
_root.add_child(_scroll)
_stack = Control.new()
_stack.custom_minimum_size = Vector2(350, 0)
_scroll.add_child(_stack)
for i in TABS.size():
var page := VBoxContainer.new()
page.custom_minimum_size = Vector2(350, 0)
page.add_theme_constant_override("separation", 3)
page.visible = (i == 0)
_stack.add_child(page)
_pages.append(page)
_tab_btns[0].disabled = true
_apply_mobile_layout()
func _apply_mobile_layout() -> void:
if _root == null or _title == null or _tabrow == null or _scroll == null or _stack == null:
return
if _mobile_mode:
_root.size = MOBILE_SIZE
_root.position = Vector2(-MOBILE_SIZE.x * 0.5, -MOBILE_SIZE.y * 0.5)
_title.visible = false
_info.position = Vector2(18, 46)
_info.size = Vector2(580, 26)
_info.add_theme_font_size_override("font_size", 14)
_mark.position = Vector2(624, 46)
_mark.size = Vector2(48, 36)
_tabrow.position = Vector2(18, 78)
_tabrow.size = Vector2(664, 40)
for b in _tab_btns:
b.custom_minimum_size = Vector2(160, 38)
b.add_theme_font_size_override("font_size", 13)
_scroll.custom_minimum_size = Vector2(664, 194)
_scroll.position = Vector2(18, 124)
_scroll.size = Vector2(664, 194)
_stack.custom_minimum_size = Vector2(648, 0)
for page in _pages:
page.custom_minimum_size = Vector2(648, 0)
page.add_theme_constant_override("separation", 6)
else:
_root.size = Vector2(380, 420)
_root.position = Vector2(-190, -210)
_title.visible = true
_title.position = Vector2(12, 8)
_info.position = Vector2(12, 32)
_info.remove_theme_font_size_override("font_size")
_mark.position = Vector2(320, 6)
_mark.size = Vector2(48, 36)
_tabrow.position = Vector2(12, 54)
_tabrow.size = Vector2(0, 0)
for b in _tab_btns:
b.custom_minimum_size = Vector2(84, 26)
b.remove_theme_font_size_override("font_size")
_scroll.custom_minimum_size = Vector2(356, 322)
_scroll.position = Vector2(12, 86)
_scroll.size = Vector2(356, 322)
_stack.custom_minimum_size = Vector2(350, 0)
for page in _pages:
page.custom_minimum_size = Vector2(350, 0)
page.add_theme_constant_override("separation", 3)