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
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
# guild_refine_test —— 公会窗 + 精炼对话框 headless 自检。
|
||||
# godot --headless --path project --script guild_refine_test.gd
|
||||
extends SceneTree
|
||||
|
||||
const GuildUI = preload("res://ui/guild_ui.gd")
|
||||
const RefineUI = preload("res://ui/refine_ui.gd")
|
||||
|
||||
class FakeClient extends Node:
|
||||
signal guild_changed()
|
||||
signal refine_ask(info: Dictionary)
|
||||
var guild := {"in_guild": false}
|
||||
var members := []
|
||||
var grades := []
|
||||
var calls := []
|
||||
func is_in_game() -> bool: return true
|
||||
func get_guild() -> Dictionary: return guild
|
||||
func get_guild_members() -> Array: return members
|
||||
func get_guild_grades() -> Array: return grades
|
||||
func refine(pos, type) -> bool: calls.append(["refine", pos, type]); return true
|
||||
|
||||
var _fail := 0
|
||||
func _ck(c: bool, m: String) -> void:
|
||||
if not c:
|
||||
_fail += 1
|
||||
printerr("FAIL: " + m)
|
||||
|
||||
func _init() -> void:
|
||||
_run()
|
||||
if _fail == 0:
|
||||
print("PASS: guild_refine_test")
|
||||
quit(0)
|
||||
else:
|
||||
printerr("%d check(s) failed" % _fail)
|
||||
quit(1)
|
||||
|
||||
func _run() -> void:
|
||||
var canvas := CanvasLayer.new()
|
||||
get_root().add_child(canvas)
|
||||
var fc := FakeClient.new()
|
||||
get_root().add_child(fc)
|
||||
|
||||
# --- 公会窗 ---
|
||||
var gu: Node = GuildUI.new()
|
||||
get_root().add_child(gu)
|
||||
gu.setup(fc, canvas)
|
||||
gu.toggle()
|
||||
_ck(gu.is_open(), "公会窗打开")
|
||||
_ck(gu._info.text.contains("未加入"), "无公会 -> 提示")
|
||||
fc.guild = {"in_guild": true, "name": "Dragons", "level": 12, "member_count": 2,
|
||||
"max_member_count": 32, "gold": 500000}
|
||||
fc.grades = [{"name": "会长", "auth": 15}, {"name": "官员", "auth": 7}, {"name": "会员", "auth": 0}]
|
||||
fc.members = [
|
||||
{"pid": 1, "name": "Master", "grade": 1, "level": 90, "offer": 99999, "general": true},
|
||||
{"pid": 2, "name": "Newbie", "grade": 3, "level": 5, "offer": 10, "general": false},
|
||||
]
|
||||
fc.guild_changed.emit()
|
||||
_ck(gu._info.text.contains("Dragons") and gu._info.text.contains("Lv.12"), "公会信息行")
|
||||
_ck(gu._pages[0].get_child_count() == 2, "2 个成员行")
|
||||
# 会长排前(grade 1 < grade 3)
|
||||
_ck(String((gu._pages[0].get_child(0) as Label).text).begins_with("Master"), "按 grade 排序")
|
||||
_ck(String((gu._pages[0].get_child(0) as Label).text).contains("[会长]"), "grade 名解析")
|
||||
|
||||
# --- 精炼对话框 ---
|
||||
var ru: Node = RefineUI.new()
|
||||
get_root().add_child(ru)
|
||||
ru.setup(fc, canvas)
|
||||
_ck(not ru.is_open(), "精炼框初始隐藏")
|
||||
fc.refine_ask.emit({
|
||||
"type": 1, "pos": 7, "src_vnum": 11209, "result_vnum": 11219,
|
||||
"cost": 200000, "prob": 45,
|
||||
"materials": [{"vnum": 30001, "count": 3}, {"vnum": 30002, "count": 1}],
|
||||
})
|
||||
_ck(ru.is_open(), "refine_ask -> 弹出")
|
||||
_ck(ru._text.text.contains("45%") and ru._text.text.contains("200000"), "成功率 + 费用")
|
||||
_ck(ru._text.text.contains("#30001 ×3"), "材料行")
|
||||
# 找「精炼」按钮
|
||||
var ok_btn: Button = null
|
||||
for b in ru._root.find_children("*", "Button", true, false):
|
||||
if b.text == "精炼":
|
||||
ok_btn = b
|
||||
_ck(ok_btn != null, "有「精炼」按钮")
|
||||
if ok_btn:
|
||||
ok_btn.pressed.emit()
|
||||
_ck(fc.calls == [["refine", 7, 1]], "精炼 -> client.refine(pos=7, type=1)")
|
||||
_ck(not ru.is_open(), "确认后关闭")
|
||||
# 取消
|
||||
fc.refine_ask.emit({"type": 0, "pos": 3, "src_vnum": 1, "result_vnum": 2, "cost": 0, "prob": 100, "materials": []})
|
||||
for b in ru._root.find_children("*", "Button", true, false):
|
||||
if b.text == "取消":
|
||||
b.pressed.emit()
|
||||
_ck(not ru.is_open() and fc.calls.size() == 1, "取消 -> 关闭且不发包")
|
||||
Reference in New Issue
Block a user