# 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 "" % 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")