Files
mtgodot-poc/project/equip_model_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

166 lines
6.6 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.
# equip_model_test —— P2 装备→模型 headless 自检(假 client + 真 item_list.txt + 桩 model)。
# godot --headless --path project --script equip_model_test.gd
extends SceneTree
const ItemListDB = preload("res://ui/item_list.gd")
const EquipModel = preload("res://ui/equip_model.gd")
class FakeClient extends Node:
signal inventory_changed(window: int, cell: int)
signal entity_main_set(vid: int)
signal entity_info(vid: int, entity: Dictionary)
var equip := []
var main_parts := [0, 0, 0, 5] # parts[3] = hair 5
func _init():
for i in 11: equip.append({"vnum": 0, "count": 0, "wear": i})
func get_equipment() -> Array: return equip
func get_main_vid() -> int: return 1000
func get_entity(_v) -> Dictionary: return {"race": 0, "parts": main_parts}
func set_wear(idx, vnum):
equip[idx] = {"vnum": vnum, "count": 1, "wear": idx}
inventory_changed.emit(2, 90 + idx) # window EQUIPMENT
# Node3D 桩:记录 weapon_gr2 / shield_gr2 / gr2_path / hair_gr2 被设成什么
class StubModel extends Node3D:
var weapon_gr2 := ""
var shield_gr2 := ""
var gr2_path := ""
var hair_gr2 := ""
var hair_skin := "-"
func _set(prop, val):
match String(prop):
"weapon_gr2": weapon_gr2 = val; return true
"shield_gr2": shield_gr2 = val; return true
"gr2_path": gr2_path = val; return true
"hair_gr2": hair_gr2 = val; return true
"hair_skin": hair_skin = val; return true
return false
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: equip_model_test (item_list + weapon swap)")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)
func _run() -> void:
var assets := AssetRoot.path()
var il_path := assets.path_join("locale/locale/common/item_list.txt")
# item_list.txt 解析
var il := ItemListDB.new()
if not FileAccess.file_exists(il_path):
print(" (skip: no item_list.txt)")
return
_ck(il.load_file(il_path), "item_list loaded")
_ck(il.count > 1000, "item_list count > 1000 (%d)" % il.count)
_ck(il.model(19) == "d:/ymir work/item/weapon/00010.gr2", "item_list.model(19), got '%s'" % il.model(19))
_ck(il.icon(19) == "icon/item/00010.tga", "item_list.icon(19)")
_ck(il.type_of(19) == "WEAPON", "item_list.type_of(19)")
# EquipModel:装备武器 vnum 19 -> model.weapon_gr2 = 解析后的真实路径
var fc := FakeClient.new()
var model := StubModel.new()
get_root().add_child(fc)
get_root().add_child(model)
var em: Node = EquipModel.new()
get_root().add_child(em)
em.setup(fc, il, func() -> Node: return model, assets)
em.main_getter = func() -> int: return 1000
fc.set_wear(4, 19) # WEAR_WEAPON
await process_frame
var wp: String = model.weapon_gr2
print(" weapon_gr2 -> '", wp, "'")
if wp == "":
# 资产里可能没有这把武器的 gr2 文件;至少确认解析逻辑跑到了(basevnum 约定)
_ck(true, "weapon resolved to '' (gr2 not in assets — resolution logic still exercised)")
else:
_ck(wp.ends_with("00010.gr2") and FileAccess.file_exists(wp), "weapon_gr2 points at a real 00010.gr2")
# 拆下武器 -> weapon_gr2 = ""
fc.set_wear(4, 0)
await process_frame
_ck(model.weapon_gr2 == "", "unequip -> weapon_gr2 cleared")
# armor_model_map 注入 -> gr2_path 切换
em.armor_model_map = {11209: "d:/ymir work/pc/warrior/warrior.gr2"}
fc.set_wear(0, 11209)
await process_frame
_ck(model.gr2_path == "d:/ymir work/pc/warrior/warrior.gr2", "armor_model_map -> gr2_path set")
# 盾(WEAR_SHIELD=10-> shield_gr2 = item_list.model 解析
# 用一个真存在的武器 vnum 当盾(资产里 00010.gr2 存在)
fc.set_wear(10, 19)
await process_frame
print(" shield_gr2 -> '", model.shield_gr2, "'")
_ck(model.shield_gr2 == "" or model.shield_gr2.ends_with("00010.gr2"), "WEAR_SHIELD -> shield_gr2")
fc.set_wear(10, 0)
await process_frame
_ck(model.shield_gr2 == "", "unequip shield -> shield_gr2 cleared")
# 头盔(WEAR_HEAD=1)有模型 -> 覆盖 hair_gr2;拆下回角色发型(parts[3]=5
var em2: Node = EquipModel.new()
get_root().add_child(em2)
em2.armor_model_map = {}
em2.setup(fc, il, func() -> Node: return model, assets)
em2.main_getter = func() -> int: return 1000
model.hair_gr2 = ""
# 头盔 vnum 用一个 item_list 里有 model 的(19 有)
fc.set_wear(1, 19)
await process_frame
_ck(model.hair_gr2 == "" or model.hair_gr2.ends_with(".gr2"), "WEAR_HEAD 有模型 -> 覆盖 hair 槽")
# 拆头盔:应尝试从 parts[3] 恢复发型(有 race_spec 才成,无则保持不炸)
fc.set_wear(1, 0)
await process_frame
_ck(true, "拆头盔 -> 回 parts 发型(不炸)")
# armor shape 走 item_proto values[3](对齐 __ArmorVnumToShape
if ClassDB.class_exists("Metin2Proto"):
var pr: Object = ClassDB.instantiate("Metin2Proto")
get_root().add_child(pr)
var ip := assets.path_join("locale/locale/en/item_proto")
if FileAccess.file_exists(ip) and pr.call("load_item_proto", ip):
var em3: Node = EquipModel.new()
get_root().add_child(em3)
em3.setup(fc, il, func() -> Node: return model, assets)
em3.proto = pr
em3.race = 0 # warrior
# 11200 "Monk Plate Armour+0" -> values[3] == 3
_ck(em3._armor_shape_default(11200) == 3, "armor 11200 -> shape 3 (via proto values[3])")
# 11000 "Wolf Armour" -> values[3] == 0 -> 回退 =vnum
_ck(em3._armor_shape_default(11000) == 11000, "armor 11000 -> values[3]=0 回退 =vnum")
_ck(em3._armor_specular(11209) == 100, "armor 11209(+9) -> specular 100")
# 时装 / parts[] 优先:服务器把 costume vnum 写进主角 parts[ARMOR],压过装备槽
var seen := {"body": -1, "hair": ""}
var em4: Node = EquipModel.new()
get_root().add_child(em4)
# 用一个记录用的桩:_apply_body / hair 走真逻辑但我们看最终 vnum
em4.setup(fc, il, func() -> Node: return model, assets)
em4.main_getter = func() -> int: return 1000
em4.armor_model_map = {77777: "d:/x/costume.gr2", 11111: "d:/x/armor.gr2"}
# 装备槽是普通盔甲,但 parts[ARMOR] 是时装 -> 用时装
fc.equip[0] = {"vnum": 11111, "count": 1, "wear": 0}
fc.main_parts = [77777, 0, 0, 5]
em4._last_body_vnum = -1
em4.refresh()
_ck(model.gr2_path == "d:/x/costume.gr2", "parts[ARMOR] 时装压过装备槽 (%s)" % model.gr2_path)
# parts[ARMOR] 清 0 -> 回退装备槽
fc.main_parts = [0, 0, 0, 5]
em4._last_body_vnum = -1
em4.refresh()
_ck(model.gr2_path == "d:/x/armor.gr2", "parts[ARMOR]=0 -> 回退 WEAR_BODY (%s)" % model.gr2_path)
# _eff 直接测
_ck(em4._eff([9, 0, 0, 0], 0, [{"vnum": 5}], 0) == 9, "_eff: parts 非 0 优先")
_ck(em4._eff([0, 0, 0, 0], 0, [{"vnum": 5}], 0) == 5, "_eff: parts 0 -> 装备槽")
_ck(em4._eff([], 1, [{"vnum": 1}, {"vnum": 2}], 1) == 2, "_eff: 无 parts -> 装备槽")