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,110 @@
|
||||
# guild_war_skill_test —— 公会窗「技能 / 公会战」两页 headless 自检。
|
||||
# godot --headless --path project --script guild_war_skill_test.gd
|
||||
extends SceneTree
|
||||
|
||||
const GuildUI = preload("res://ui/guild_ui.gd")
|
||||
|
||||
class FakeTable extends Node:
|
||||
func for_category(cat: String) -> Array:
|
||||
return [4051, 4052, 4053] if cat == "GUILD" else []
|
||||
func name_of(id: int) -> String:
|
||||
return {4051: "축복", 4052: "미리내", 4053: "무장"}.get(id, "skill %d" % id)
|
||||
|
||||
class FakeClient extends Node:
|
||||
signal guild_changed()
|
||||
signal guild_skill_changed()
|
||||
signal guild_war_changed()
|
||||
var guild := {"in_guild": true, "id": 77, "name": "Dragons", "level": 12,
|
||||
"member_count": 2, "max_member_count": 32, "gold": 5}
|
||||
var skill := {"valid": false}
|
||||
var wars := []
|
||||
var war := {"opp_guild_id": 0, "opp_name": "", "type": 0, "state": 0}
|
||||
var calls := []
|
||||
func is_in_game() -> bool: return true
|
||||
func get_guild() -> Dictionary: return guild
|
||||
func get_guild_members() -> Array: return []
|
||||
func get_guild_grades() -> Array: return []
|
||||
func get_guild_skill() -> Dictionary: return skill
|
||||
func get_guild_wars() -> Array: return wars
|
||||
func get_guild_war() -> Dictionary: return war
|
||||
func use_guild_skill(vnum: int, target: int) -> bool:
|
||||
calls.append(["use", vnum, target]); return true
|
||||
func declare_guild_war(name: String) -> bool:
|
||||
calls.append(["war", name]); 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_war_skill_test")
|
||||
quit(0)
|
||||
else:
|
||||
printerr("%d check(s) failed" % _fail)
|
||||
quit(1)
|
||||
|
||||
func _btn(root: Node, label: String) -> Button:
|
||||
for b in root.find_children("*", "Button", true, false):
|
||||
if String(b.text) == label:
|
||||
return b
|
||||
return null
|
||||
|
||||
func _labels_text(page: Node) -> String:
|
||||
var s := ""
|
||||
for c in page.find_children("*", "Label", true, false):
|
||||
s += String(c.text) + "\n"
|
||||
return s
|
||||
|
||||
func _run() -> void:
|
||||
var canvas := CanvasLayer.new()
|
||||
get_root().add_child(canvas)
|
||||
var fc := FakeClient.new()
|
||||
get_root().add_child(fc)
|
||||
var tbl := FakeTable.new()
|
||||
get_root().add_child(tbl)
|
||||
|
||||
var gu: Node = GuildUI.new()
|
||||
get_root().add_child(gu)
|
||||
gu.setup(fc, canvas, tbl)
|
||||
gu.toggle()
|
||||
_ck(gu.is_open(), "公会窗打开")
|
||||
_ck(gu._pages.size() == 3, "3 页")
|
||||
|
||||
# --- 技能页 ---
|
||||
gu._set_tab(1)
|
||||
_ck(_labels_text(gu._pages[1]).contains("还没收到"), "技能页:无数据提示")
|
||||
|
||||
fc.skill = {"valid": true, "skill_point": 5, "guild_point": 1200, "max_guild_point": 5000,
|
||||
"levels": [3, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 7]}
|
||||
fc.guild_skill_changed.emit()
|
||||
var st := _labels_text(gu._pages[1])
|
||||
_ck(st.contains("技能点 5") and st.contains("1200 / 5000"), "技能页:点数行")
|
||||
_ck(st.contains("축복") and st.contains("Lv 3"), "技能页:GUILD 技能名 + 等级")
|
||||
_ck(st.contains("무장") and st.contains("Lv 7"), "技能页:第 12 个技能配到 levels[11]")
|
||||
# 축복 lv3 -> 有「施放」按钮;미리내 lv0 -> 无
|
||||
var cast := _btn(gu._pages[1], "施放")
|
||||
_ck(cast != null, "技能页:可施放技能有按钮")
|
||||
cast.pressed.emit()
|
||||
_ck(fc.calls == [["use", 4051, 0]], "施放 -> use_guild_skill(4051, 0)")
|
||||
|
||||
# --- 公会战页 ---
|
||||
gu._set_tab(2)
|
||||
_ck(_labels_text(gu._pages[2]).contains("当前无公会战"), "公会战页:无战提示")
|
||||
_ck(_labels_text(gu._pages[2]).contains("无"), "公会战页:进行中列表为空")
|
||||
|
||||
# 宣战
|
||||
gu._war_name_edit.text = "Tigers"
|
||||
_btn(gu._pages[2], "宣战").pressed.emit()
|
||||
_ck(fc.calls[-1] == ["war", "Tigers"], "宣战 -> declare_guild_war(\"Tigers\")")
|
||||
|
||||
# 收到 WAR ON_WAR + WAR_LIST
|
||||
fc.war = {"opp_guild_id": 88, "opp_name": "Tigers", "type": 1, "state": 6}
|
||||
fc.wars = [{"src": 77, "dst": 88, "src_name": "Dragons", "dst_name": "Tigers"}]
|
||||
fc.guild_war_changed.emit()
|
||||
var wt := _labels_text(gu._pages[2])
|
||||
_ck(wt.contains("对 Tigers") and wt.contains("交战中"), "公会战页:当前战状态")
|
||||
_ck(wt.contains("Dragons vs Tigers") and wt.contains("◀我方"), "公会战页:GvG 行 + 我方标记")
|
||||
Reference in New Issue
Block a user