Files
mtgodot-poc/project/ui/guild_ui.gd
T
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
2026-08-31 20:02:12 +09:00

315 lines
10 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: RefCounted # SkillTable
# 返回一张 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
func setup(m2client: Node, parent: Node, table: RefCounted = null) -> void:
client = m2client
skill_table = table
_build(parent)
for sig in ["guild_changed", "guild_skill_changed", "guild_war_changed"]:
if client.has_signal(sig):
client.connect(sig, refresh)
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)))
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("general", false) else ""]
page.add_child(row)
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 "✘ 上传失败"
# 公会 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