Files
mtgodot-poc/project/ui/guild_ui.gd
T
shenleiandClaude Code 8092ff44e2 装备校验链/快捷栏/公会/好友情侣/交易仓库五项差距修复(增量 129)
- 装备校验:can_equip 按 CanEquipNow→FindEquipCell→EquipItem 精确重排,
  新增 find_equip_cell() 1:1 复刻(RING/UNIQUE 占用翻转、COSTUME/BELT/DS),
  帝国 antiflag 降级为 tooltip 提示,LIMIT_PCBANG/REAL_TIME* 语义与倒计时文案
- 快捷栏:死亡全槽禁用+激活守卫、变身(affect 220)技能置灰、
  物品槽失配置灰(服务器回收/换武器后刷新)
- 公会:GUILD_AUTH_* 权限位门控踢人/公告/技能升级,/gskillup、
  被宣战应答弹窗(/war·/nowar)、资金存取行(40250 服务端已禁用仍保留 1:1)
- 好友/情侣:messenger 三固定组(好友/公会/情侣),lover_login/logout/
  near/far/divorce 命令事件→在线/在身边状态与离婚隐藏
- 交易/仓库:ExchangeState.last_notice(ALREADY/LESS_GOLD)结束浮层提示;
  仓库改密 /safebox_change_password 三段输入;关窗补 /safebox_close·/mall_close;
  仓库金钱经核实 40250 无 wire 协议,UI 定界只读
- app_flow:重连时 guild_marks_ready 重复连接加 is_connected 守卫
- 相机 look_at 零向量守卫(首帧/传送落点)
- 测试:equip/love/guild 断言更新,79 项中 78 绿(game_camera_test 为
  HEAD 既有失败);真服冒烟 LIVE_SMOKE PASS(192.168.21.203 全链路)
- docs/CLIENT-GAP.md 同步增量 129

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kUFvQGYusuY3dkfGitmAe
2026-09-07 21:43:30 +09:00

692 lines
26 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
var _war_ask_dialog: ConfirmationDialog
# GUILD_AUTH_*(服务器 guild.h:94-97):等级权限位。会长(grade 0)恒全权。
const AUTH_ADD_MEMBER := 1 << 0
const AUTH_REMOVE_MEMBER := 1 << 1
const AUTH_NOTICE := 1 << 2
const AUTH_USE_SKILL := 1 << 3
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)
# GUILD_WAR_RECV_DECLARE(3) 时弹被宣战应答框(原版 AcceptGuildWarDialog
# 接受发 /war <name>,拒绝发 /nowar <name>,均走 chat 通道)。
if client.has_signal("guild_war_event"):
client.guild_war_event.connect(_on_guild_war_event)
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()
# 我的等级权限位(会长 grade 0 恒 0xF;否则取 grades[grade-1].auth)。
# 成员/等级数据未同步(成员表为空、找不到自己)时**返回全权**——权限只是 UI 预判,
# 不可判定时不拦操作,由服务器权威兜底(与原版一致:原版也不在前端做这层门)。
const AUTH_ALL := AUTH_ADD_MEMBER | AUTH_REMOVE_MEMBER | AUTH_NOTICE | AUTH_USE_SKILL
func _my_auth() -> int:
if client == null or not client.has_method("get_guild_grades") \
or not client.has_method("get_main_pid") \
or not client.has_method("get_guild_members"):
return AUTH_ALL
var my_pid := int(client.get_main_pid())
var my_grade := -1
for m in client.get_guild_members():
if int(m.get("pid", 0)) == my_pid:
my_grade = int(m.get("grade", -1))
break
if my_grade == -1:
return AUTH_ALL # 成员表里还没有自己(未同步)
if my_grade == 0:
return AUTH_ALL # GUILD_LEADER_GRADE
var grades: Array = client.get_guild_grades()
if my_grade >= 1 and my_grade <= grades.size():
return int(grades[my_grade - 1].get("auth", 0))
return AUTH_ALL
# 被宣战应答(对齐原版 __GuildWar_OpenAskDialog / OnAccept / OnDecline)。
func _on_guild_war_event(state: int, opp_guild_id: int, opp_name: String) -> void:
if state != 3 or opp_guild_id == 0: # GUILD_WAR_RECV_DECLARE
return
if is_instance_valid(_war_ask_dialog):
_war_ask_dialog.queue_free()
_war_ask_dialog = ConfirmationDialog.new()
_war_ask_dialog.title = "公会战邀请"
var shown := opp_name if opp_name != "" else "#%d" % opp_guild_id
_war_ask_dialog.dialog_text = "公会 [%s] 向我方宣战,接受吗?" % shown
_war_ask_dialog.ok_button_text = "接受"
_war_ask_dialog.cancel_button_text = "拒绝"
_war_ask_dialog.confirmed.connect(func() -> void:
if client.has_method("declare_guild_war"):
client.declare_guild_war(opp_name) # /war <name>
_war_ask_dialog.queue_free()
_war_ask_dialog = null)
_war_ask_dialog.canceled.connect(func() -> void:
if client.has_method("say") and opp_name != "":
client.say(0, "/nowar " + opp_name) # 原版 __GuildWar_OnDecline
_war_ask_dialog.queue_free()
_war_ask_dialog = null)
if _root and _root.get_parent():
_root.get_parent().add_child(_war_ask_dialog)
if _mobile_mode:
_war_ask_dialog.set_meta("mobile_modal", true)
_war_ask_dialog.set_meta("mobile_title", "公会战邀请")
_war_ask_dialog.popup_centered()
func _my_grade_is_leader() -> bool:
if client == null or not client.has_method("get_main_pid") \
or not client.has_method("get_guild_members"):
return false
var my_pid := int(client.get_main_pid())
for m in client.get_guild_members():
if int(m.get("pid", 0)) == my_pid:
return int(m.get("grade", -1)) == 0 # GUILD_LEADER_GRADE
return false
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()
# 公会资金存取(GUILD_CG_DEPOSIT/WITHDRAW_MONEY = 13/14)。40250 服务器把
# 两个处理分支都禁用了(input_main.cpp 直接 return),但原版 uiguild.py 的
# OnDeposit/OnWithdraw 仍照发——保持 1:1,结果以服务器回执为准。
if client.has_method("guild_deposit_money"):
var money := HBoxContainer.new()
money.add_theme_constant_override("separation", 6)
page.add_child(money)
var ginfo: Dictionary = client.get_guild()
var mlabel := Label.new()
mlabel.add_theme_font_size_override("font_size", 14 if _mobile_mode else 12)
mlabel.custom_minimum_size = Vector2(300, 40) if _mobile_mode else Vector2(160, 0)
mlabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
mlabel.text = "公会资金: %d" % int(ginfo.get("gold", 0))
money.add_child(mlabel)
var amount := LineEdit.new()
amount.placeholder_text = "金额"
amount.max_length = 9
amount.custom_minimum_size = Vector2(160, 40) if _mobile_mode else Vector2(96, 26)
money.add_child(amount)
var dep := Button.new()
dep.text = "存入"
dep.tooltip_text = "40250 服务器已禁用公会存款"
dep.custom_minimum_size = Vector2(92, 40) if _mobile_mode else Vector2(56, 26)
dep.pressed.connect(func():
var v := int(amount.text)
if v > 0:
client.guild_deposit_money(v))
money.add_child(dep)
var wdr := Button.new()
wdr.text = "取出"
wdr.tooltip_text = "40250 服务器已禁用公会取款"
wdr.custom_minimum_size = Vector2(92, 40) if _mobile_mode else Vector2(56, 26)
wdr.pressed.connect(func():
var v := int(amount.text)
if v > 0:
client.guild_withdraw_money(v))
money.add_child(wdr)
var can_kick := (_my_auth() & AUTH_REMOVE_MEMBER) != 0
var my_pid := int(client.get_main_pid()) if client.has_method("get_main_pid") else 0
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 mrow := HBoxContainer.new()
mrow.add_theme_constant_override("separation", 6)
page.add_child(mrow)
var row := Label.new()
row.custom_minimum_size = Vector2(560, 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 "")]
mrow.add_child(row)
# 成员管理:REMOVE_MEMBER 权限位可见的踢出按钮(自己与会长不可踢,服务器兜底)。
var pid := int(m.get("pid", 0))
if can_kick and pid != 0 and pid != my_pid and gi != 0:
var kick := Button.new()
kick.text = "踢出"
kick.custom_minimum_size = Vector2(72, 34) if _mobile_mode else Vector2(44, 24)
kick.pressed.connect(func():
if client.has_method("guild_remove_member"):
client.guild_remove_member(pid))
mrow.add_child(kick)
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 board_status := Label.new()
board_status.add_theme_font_size_override("font_size", 11)
board_status.add_theme_color_override("font_color", Color(1, 0.65, 0.55))
page.add_child(board_status)
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 == "":
return
# 服务器 input_main.cpp:2819'!' 前缀(公告)需要 GUILD_AUTH_NOTICE。
if text.begins_with("!") and (_my_auth() & AUTH_NOTICE) == 0:
board_status.text = "发布公告需要相应权限"
return
board_status.text = ""
if 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()
var auth := _my_auth()
var can_use_skill := (auth & AUTH_USE_SKILL) != 0
# do_guildskillup 只认会长(GUILD_LEADER_GRADE);技能点 > 0 才显示升级按钮。
var is_leader := auth == AUTH_ALL and _my_grade_is_leader()
var has_point := int(sk.get("skill_point", 0)) > 0
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)
# 施放:服务器 guild.cpp:1248 要求 GUILD_AUTH_USE_SKILL。
if sid >= 0 and lv > 0 and can_use_skill:
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)
# 升级:/gskillup <技能 vnum>cmd_general.cpp do_guildskillup,仅会长)。
if sid >= 0 and is_leader and has_point:
var up := Button.new()
up.text = "升级"
up.custom_minimum_size = Vector2(92, 40) if _mobile_mode else Vector2(56, 24)
up.pressed.connect(func():
if client.has_method("say"):
client.say(0, "/gskillup %d" % sid))
row.add_child(up)
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)