# skill_test —— P6:skilldesc 解析 + skill_ui 行 + quickbar 施放/冷却 headless 自检。 # godot --headless --path project --script skill_test.gd extends SceneTree const SkillTable = preload("res://ui/skill_table.gd") const SkillUI = preload("res://ui/skill_ui.gd") const Quickbar = preload("res://ui/quickbar.gd") const UiManager = preload("res://ui/ui_manager.gd") class FakeClient extends Node: signal skills_changed() signal skill_cooldown_end(skill: int) signal quickslots_changed() var skills := [{"id": 1, "level": 5, "master": 0}, {"id": 16, "level": 3, "master": 2}] var quickslots := [] var casts := [] var skill_uses := [] var quickslot_ops := [] var saids := [] var main := 1000 func get_main_vid() -> int: return main func get_entity(_v) -> Dictionary: return {"race": 0} func get_skills() -> Array: return skills func get_quickslots() -> Array: return quickslots func get_target() -> Dictionary: return {"vid": 2000} func cast_skill(mi, rot, x, y) -> bool: casts.append([mi, rot, x, y]); return true func use_skill(id, target_vid) -> bool: skill_uses.append([id, target_vid]); return true func quickslot_add(pos, type, ref) -> bool: quickslot_ops.append(["add", pos, type, ref]); return true func quickslot_del(pos) -> bool: quickslot_ops.append(["del", pos]); return true func quickslot_swap(pos, change_pos) -> bool: quickslot_ops.append(["swap", pos, change_pos]); return true func skill_up(id) -> bool: saids.append(id); return true func use_item(w, c) -> bool: 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: skill_test (table + skill_ui + quickbar)") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1) func _run() -> void: var assets := AssetRoot.path() var st := SkillTable.new() var loaded := false for lang in ["en", "common"]: if st.load_file(assets.path_join("locale/locale/%s/skilldesc.txt" % lang)): loaded = true break if not loaded: print(" (skip: no skilldesc.txt)") return _ck(st.count > 30, "skilldesc parsed (%d skills)" % st.count) var w := st.for_job("WARRIOR") _ck(w.size() > 3, "WARRIOR has skills (%d)" % w.size()) var s1: Dictionary = st.entry(1) _ck(s1.get("job", "") == "WARRIOR", "skill 1 job = WARRIOR") _ck(String(s1.get("name", "")) != "", "skill 1 has a name (%s)" % s1.get("name")) # 列偏移修正后:skill 1 'Three-Way Cut' motion_name=samyeon motion_idx=1, ATTACK_SKILL _ck(st.motion_idx_of(1) == 1, "skill 1 motion_idx == 1 (列偏移已修)") _ck(String(s1.get("motion", "")) == "samyeon", "skill 1 motion_name == samyeon") _ck(st.is_attack(1), "skill 1 是 ATTACK_SKILL") _ck(st.is_ranged(46), "skill 46 bow skill is ranged") _ck(not st.is_ranged(1), "skill 1 melee skill is not ranged") _ck(is_equal_approx(st.cooldown_of(1, 5), 15.0), "skill 1 .msk CoolTimeFormula = 15s") _ck(st.is_attack(19) == false and String(st.entry(19).get("name", "")) != "", "skill 19 'Strong Body' 非攻击") # 辅助 / 被动树 var sup := st.for_category("SUPPORT") _ck(sup.size() > 3, "SUPPORT 分类有技能 (%d)" % sup.size()) _ck(st.is_passive(121), "skill 121 'Leadership' is_passive") _ck(st.can_level_up(125) == false, "skill 125 'Item Creation' CANNOT_LEVEL_UP") _ck(st.for_category("HORSE").size() >= 3, "HORSE 分类有技能") var fc := FakeClient.new() get_root().add_child(fc) var ui: CanvasLayer = UiManager.new() get_root().add_child(ui) ui._ready() # skill_ui var sk: Node = SkillUI.new() get_root().add_child(sk) sk.setup(fc, st, ui) sk.set_job("WARRIOR") sk.open() _ck(sk.is_open(), "skill window opened") _ck(sk._rows.size() == w.size(), "skill window rows = WARRIOR skill count") # 找 skill 1 的行,点加号 if sk._rows.has(1): sk._rows[1]["up"].pressed.emit() _ck(fc.saids == [1], "+ -> client.skill_up(1)") _ck(String(sk._rows[1]["lv"].text) == "Lv 5", "skill 1 shows Lv 5 (from get_skills)") # skill 16 master 2 -> "Lv 3 G" if sk._rows.has(16): _ck(String(sk._rows[16]["lv"].text) == "Lv 3 G", "skill 16 master 2 -> 'Lv 3 G'") # 切到「辅助」页 -> 列 SUPPORT,被动技能无 [+] sk._switch_tab(1) _ck(sk._cat_key() == "SUPPORT", "tab 1 -> SUPPORT") _ck(sk._rows.has(121) and sk._rows[121]["up"] == null, "被动技能 121 无 [+] 按钮") # 有能加点的辅助技能带 [+] var any_up := false for rid in sk._rows: if sk._rows[rid]["up"] != null: any_up = true _ck(any_up or sup.all(func(i): return st.is_passive(i) or not st.can_level_up(i)), "辅助页有可加点项 或 全是被动") sk._switch_tab(2) _ck(sk._cat_key() == "HORSE", "tab 2 -> HORSE") sk._switch_tab(0) # 切回主动,后面的 quickbar 断言不受影响 # quickbar var pl := Node3D.new() get_root().add_child(pl) var qb: Node = Quickbar.new() get_root().add_child(qb) qb.setup(fc, st, ui, func() -> Node: return pl) qb.assign(0, "skill", 1) _ck(fc.quickslot_ops == [["add", 0, 2, 1]], "assign -> CG_QUICKSLOT_ADD slot 0") qb.activate(0) _ck(fc.skill_uses.size() == 1 and fc.skill_uses[0] == [1, 2000], "quickbar slot 0 -> use_skill(skill 1, selected target)") _ck(fc.casts.size() == 1, "quickbar slot 0 -> cast_skill once") if fc.casts.size() == 1: _ck(int(fc.casts[0][0]) == st.motion_idx_of(1), "cast used skill 1's motion_idx") # 冷却中不再施放 qb.activate(0) _ck(fc.skill_uses.size() == 1, "on cooldown -> no second use_skill") _ck(fc.casts.size() == 1, "on cooldown -> no second cast") # GC_SKILL_COOLTIME_END 提前解锁 fc.skill_cooldown_end.emit(1) qb.activate(0) _ck(fc.skill_uses.size() == 2, "cooldown_end -> can use_skill again") _ck(fc.casts.size() == 2, "cooldown_end -> can cast again") # 36 槽 = 4 页 × 9 格;第四页的第 9 格正好是全局槽 35。 qb.set_page(3) qb.assign(8, "item", 9) _ck(qb._state.size() == 36 and qb._state[35].kind == "item" and qb._state[35].id == 9, "quickbar has 36 slots; page 4 slot 9 = global slot 35") _ck(fc.quickslot_ops.back() == ["add", 35, 1, 9], "page 4 assign -> CG_QUICKSLOT_ADD{35,item,9}") qb.set_page(0) qb._swap(0, 1) # UI 的两次点击最终调用同一个交换操作 _ck(fc.quickslot_ops.back() == ["swap", 0, 1], "quickslot move -> CG_QUICKSLOT_SWAP") qb._clear(1) _ck(fc.quickslot_ops.back() == ["del", 1], "quickslot clear -> CG_QUICKSLOT_DEL") # 服务器 GC_QUICKSLOT_* 恢复:type 2 技能 / type 1 道具 -> 填格 var qb2: Node = Quickbar.new() get_root().add_child(qb2) fc.quickslots = [{"pos": 2, "type": 2, "ref": 16}, {"pos": 3, "type": 1, "ref": 9}] qb2.setup(fc, st, ui, func() -> Node: return pl) # setup 里自动 restore_from_server _ck(qb2._state[2].kind == "skill" and int(qb2._state[2].id) == 16, "quickslot 恢复:格2=技能16") _ck(qb2._state[3].kind == "item" and int(qb2._state[3].id) == 9, "quickslot 恢复:格3=道具9") fc.quickslots = [{"pos": 0, "type": 2, "ref": 1}] fc.quickslots_changed.emit() _ck(qb2._state[0].kind == "skill" and int(qb2._state[0].id) == 1, "quickslots_changed -> 重填")