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
153 lines
6.4 KiB
GDScript
153 lines
6.4 KiB
GDScript
# char_status_ui_test —— P11 角色状态窗 headless 自检(假 client + 真 uiscript)。
|
||
# godot --headless --path project --script char_status_ui_test.gd
|
||
# 校验 characterwindow.py 装载、RefreshStatus 数值绑定(逐字对照 uicharacter.py)、
|
||
# /stat 加点命令、页签切换。缺 uiscript 资产时优雅跳过。
|
||
extends SceneTree
|
||
|
||
const UiManager = preload("res://ui/ui_manager.gd")
|
||
const CharStatusUI = preload("res://ui/char_status_ui.gd")
|
||
|
||
class FakeClient extends Node:
|
||
signal points_changed(points: Dictionary)
|
||
signal entity_main_set(vid: int)
|
||
var pts: Array = []
|
||
var says: Array = []
|
||
var _main_vid := 7
|
||
func _init() -> void:
|
||
pts.resize(255)
|
||
pts.fill(0)
|
||
pts[1] = 42 # LEVEL
|
||
pts[3] = 123456 # EXP
|
||
pts[4] = 200000 # NEXT_EXP
|
||
pts[5] = 850 # HP
|
||
pts[6] = 900 # MAX_HP
|
||
pts[7] = 120 # SP
|
||
pts[8] = 150 # MAX_SP
|
||
pts[12] = 33 # ST
|
||
pts[13] = 27 # HT
|
||
pts[14] = 18 # DX
|
||
pts[15] = 12 # IQ
|
||
pts[16] = 250 # ATT_POWER
|
||
pts[17] = 145 # ATT_SPEED
|
||
pts[18] = 40 # EVADE_RATE
|
||
pts[19] = 165 # MOV_SPEED
|
||
pts[20] = 88 # DEF_GRADE
|
||
pts[21] = 100 # CASTING_SPEED
|
||
pts[22] = 55 # MAGIC_ATT_GRADE
|
||
pts[23] = 44 # MAGIC_DEF_GRADE
|
||
pts[26] = 5 # STAT (남은 포인트)
|
||
pts[29] = 210 # MIN_ATK
|
||
pts[30] = 210 # MAX_ATK
|
||
pts[95] = 15 # ATT_GRADE_BONUS
|
||
func get_main_vid() -> int: return _main_vid
|
||
func get_points() -> Dictionary:
|
||
return {
|
||
"points": pts, "hp": pts[5], "max_hp": pts[6], "sp": pts[7], "max_sp": pts[8],
|
||
"level": pts[1], "exp": pts[3], "next_exp": pts[4], "gold": 0,
|
||
}
|
||
func get_entity(vid: int) -> Dictionary:
|
||
if vid != _main_vid:
|
||
return {}
|
||
return {"name": "TestHero", "guild": 9, "race": 5} # race 5 -> job 1 (assassin)
|
||
func get_guild_name(gid: int) -> String:
|
||
return "銀河聯盟" if gid == 9 else ""
|
||
func say(type: int, text: String) -> bool:
|
||
says.append([type, text]); 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: char_status_ui_test (layout + RefreshStatus + /stat + tabs)")
|
||
quit(0)
|
||
else:
|
||
printerr("%d check(s) failed" % _fail)
|
||
quit(1)
|
||
|
||
func _txt(cs: Node, nm: String) -> String:
|
||
var n: Control = cs._node(nm)
|
||
return String(n.text) if n and ("text" in n) else "<missing:%s>" % nm
|
||
|
||
func _run() -> void:
|
||
var assets := AssetRoot.path()
|
||
if not FileAccess.file_exists(assets.path_join("uiscript/uiscript/characterwindow.py")):
|
||
print(" (skip: no characterwindow.py — assets checkout missing)")
|
||
return
|
||
|
||
var ui: CanvasLayer = UiManager.new()
|
||
get_root().add_child(ui)
|
||
ui._ready()
|
||
var fc := FakeClient.new()
|
||
get_root().add_child(fc)
|
||
|
||
var cs: Node = CharStatusUI.new()
|
||
get_root().add_child(cs)
|
||
cs.setup(ui, fc, assets)
|
||
cs.open()
|
||
_ck(cs.is_open(), "character window opened")
|
||
if not cs.is_open():
|
||
return
|
||
|
||
# --- RefreshStatus 数值绑定 ---
|
||
_ck(_txt(cs, "Level_Value") == "42", "Level_Value = 42 (got %s)" % _txt(cs, "Level_Value"))
|
||
_ck(_txt(cs, "Exp_Value") == "123456", "Exp_Value = 123456 (got %s)" % _txt(cs, "Exp_Value"))
|
||
_ck(_txt(cs, "RestExp_Value") == "76544", "RestExp_Value = next-exp (got %s)" % _txt(cs, "RestExp_Value"))
|
||
_ck(_txt(cs, "HP_Value") == "850/900", "HP_Value = 850/900 (got %s)" % _txt(cs, "HP_Value"))
|
||
_ck(_txt(cs, "SP_Value") == "120/150", "SP_Value = 120/150 (got %s)" % _txt(cs, "SP_Value"))
|
||
_ck(_txt(cs, "STR_Value") == "33", "STR_Value = 33 (got %s)" % _txt(cs, "STR_Value"))
|
||
_ck(_txt(cs, "DEX_Value") == "18", "DEX_Value = 18 (got %s)" % _txt(cs, "DEX_Value"))
|
||
_ck(_txt(cs, "HTH_Value") == "27", "HTH_Value = 27 (got %s)" % _txt(cs, "HTH_Value"))
|
||
_ck(_txt(cs, "INT_Value") == "12", "INT_Value = 12 (got %s)" % _txt(cs, "INT_Value"))
|
||
# ATT: min==max==210, bonus = 95(15) + 91(0) -> 225
|
||
_ck(_txt(cs, "ATT_Value") == "225", "ATT_Value = 210+15 (got %s)" % _txt(cs, "ATT_Value"))
|
||
_ck(_txt(cs, "DEF_Value") == "88", "DEF_Value = 88 (got %s)" % _txt(cs, "DEF_Value"))
|
||
_ck(_txt(cs, "MATT_Value") == "55", "MATT_Value = 55 (no magic-wep) (got %s)" % _txt(cs, "MATT_Value"))
|
||
_ck(_txt(cs, "MDEF_Value") == "44", "MDEF_Value = 44 (got %s)" % _txt(cs, "MDEF_Value"))
|
||
_ck(_txt(cs, "ASPD_Value") == "145", "ASPD_Value = 145 (got %s)" % _txt(cs, "ASPD_Value"))
|
||
_ck(_txt(cs, "MSPD_Value") == "165", "MSPD_Value = 165 (got %s)" % _txt(cs, "MSPD_Value"))
|
||
_ck(_txt(cs, "CSPD_Value") == "100", "CSPD_Value = 100 (got %s)" % _txt(cs, "CSPD_Value"))
|
||
_ck(_txt(cs, "ER_Value") == "40", "ER_Value = 40 (got %s)" % _txt(cs, "ER_Value"))
|
||
_ck(_txt(cs, "Status_Plus_Value") == "5", "Status_Plus_Value = 5 (got %s)" % _txt(cs, "Status_Plus_Value"))
|
||
|
||
# --- identity ---
|
||
_ck(_txt(cs, "Character_Name") == "TestHero", "Character_Name = TestHero (got %s)" % _txt(cs, "Character_Name"))
|
||
_ck(_txt(cs, "Guild_Name") == "銀河聯盟", "Guild_Name resolved (got %s)" % _txt(cs, "Guild_Name"))
|
||
|
||
# --- +/- 按钮:STAT>0 时可见,命令与 uicharacter.statusPlusCommandDict 一致 ---
|
||
var hth_plus: Control = cs._node("HTH_Plus")
|
||
_ck(hth_plus != null and hth_plus.visible, "HTH_Plus visible while STAT>0")
|
||
if hth_plus is BaseButton:
|
||
hth_plus.pressed.emit()
|
||
_ck(fc.says.size() == 1 and fc.says[0][0] == 0 and fc.says[0][1] == "/stat ht",
|
||
"HTH_Plus -> say(0, '/stat ht') (got %s)" % str(fc.says))
|
||
var int_minus: Control = cs._node("INT_Minus")
|
||
if int_minus is BaseButton:
|
||
int_minus.pressed.emit()
|
||
_ck(fc.says.size() == 2 and fc.says[1][1] == "/stat- iq",
|
||
"INT_Minus -> say(0, '/stat- iq') (got %s)" % str(fc.says))
|
||
|
||
# --- STAT == 0 -> 加点按钮隐藏 + 刷新走 points_changed ---
|
||
fc.pts[26] = 0
|
||
fc.points_changed.emit(fc.get_points())
|
||
_ck(_txt(cs, "Status_Plus_Value") == "0", "Status_Plus_Value = 0 after change")
|
||
var hth_plus2: Control = cs._node("HTH_Plus")
|
||
_ck(hth_plus2 != null and not hth_plus2.visible, "HTH_Plus hidden while STAT==0")
|
||
|
||
# --- 页签切换(uicharacter.SetState) ---
|
||
var char_page: Control = cs._node("Character_Page")
|
||
var skill_page: Control = cs._node("Skill_Page")
|
||
_ck(char_page != null and char_page.visible, "STATUS page visible by default")
|
||
cs._set_state("SKILL")
|
||
_ck(char_page != null and not char_page.visible, "Character_Page hidden after SetState(SKILL)")
|
||
_ck(skill_page != null and skill_page.visible, "Skill_Page visible after SetState(SKILL)")
|
||
cs._set_state("STATUS")
|
||
_ck(char_page.visible, "Character_Page visible again after SetState(STATUS)")
|
||
|
||
cs.close()
|
||
_ck(not cs.is_open(), "character window closed")
|