Files
mtgodot-poc/project/skill_test.gd
T
shenandClaude Sonnet 5 47baf6c0c6 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
2026-08-31 20:02:12 +09:00

165 lines
6.9 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# skill_test —— P6skilldesc 解析 + 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 -> 重填")