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

444 lines
15 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 _info: Label
var _mark: TextureRect
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
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 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(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.add_theme_font_size_override("font_size", 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(245, 26)
actions.add_child(edit)
var post := Button.new()
post.text = "发布"
post.custom_minimum_size = 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(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(286, 0)
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
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(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.add_theme_font_size_override("font_size", 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", 12)
lbl.custom_minimum_size = Vector2(240, 0)
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(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.add_theme_font_size_override("font_size", 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(200, 26)
decl.add_child(_war_name_edit)
var db := Button.new()
db.text = "宣战"
db.custom_minimum_size = 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.add_theme_font_size_override("font_size", 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.add_theme_font_size_override("font_size", 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):
_create_dialog.popup_centered()
_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(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", 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)
_create_dialog.popup_centered()
_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")
_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")
_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()
# 公会 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)
var 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)
var 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)
var sc := ScrollContainer.new()
sc.position = Vector2(12, 86)
sc.custom_minimum_size = Vector2(356, 322)
_root.add_child(sc)
var stack := Control.new()
stack.custom_minimum_size = Vector2(350, 0)
sc.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