feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复

- 桥梁与静态物体高度采样修复:
  - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight
  - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程
  - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题
  - 新增 test_bridge_height_parity.gd 自动化对拍测试
- 40250 怪物击杀经验动效:
  - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附
- 40250 客户端全系统功能对齐(Batches 1-31):
  - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试
- 文档沉淀:
  - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
This commit is contained in:
shen
2026-09-19 08:51:25 -07:00
parent 1db9e9a129
commit 66d217b313
650 changed files with 53046 additions and 1586 deletions
+207
View File
@@ -0,0 +1,207 @@
# test_fish_clam_roast_parity.gd —— Metin2 40250 活鱼宰杀、珍珠蚌开珠与篝火炙烤盛宴 1:1 测试套件
extends SceneTree
const FishClamRoastSystem = preload("res://fish_clam_roast_system.gd")
var _passed := 0
var _failed := 0
func _assert(cond: bool, msg: String) -> void:
if cond:
_passed += 1
print(" [PASS] %s" % msg)
else:
_failed += 1
printerr(" [FAIL] %s" % msg)
func _init() -> void:
print("\n=== Running test_fish_clam_roast_parity (Direction 1: Fish Gutting & Clam Pearls 1:1) ===")
test_live_fish_gutting()
test_clam_pearl_opening()
test_campfire_grilling()
test_cooked_fish_buffs()
test_serialization()
print("\n=======================================================")
print("Fish & Clam Parity Tests: %d Passed, %d Failed" % [_passed, _failed])
print("=======================================================\n")
if _failed > 0:
quit(1)
else:
quit(0)
func test_live_fish_gutting() -> void:
print("\n--- Test 1: Live Fish Gutting (40250 1:1) ---")
var system = FishClamRoastSystem.new()
var inv: Array = [
{"vnum": 27806, "count": 1}, # 活鲤鱼 (Sazan)
{"vnum": 27808, "count": 1}, # 活草鱼 (Ot Sazanı)
{"vnum": 27803, "count": 1}, # 活鲈鱼
{"vnum": 27803, "count": 1}, # 活鲈鱼
{"vnum": 27803, "count": 1} # 活鲈鱼
]
# 1. 产生死鱼 (roll >= 4001)
var res = system.gut_live_fish(inv, 0, 5000)
_assert(res["ok"] == true, "Gut fish succeeded")
_assert(res["result"] == "dead_fish", "Result is dead_fish")
_assert(res["vnum"] == 27836, "Spawned dead Sazan (27836)")
# 2. 产生鱼骨 (roll 2001~4000)
res = system.gut_live_fish(inv, 1, 3000)
_assert(res["ok"] == true, "Gut fish succeeded for bone")
_assert(res["result"] == "fish_bone", "Result is fish_bone")
_assert(res["vnum"] == 27995, "Spawned Fish Bone (27995)")
# 3. 产生珍珠蚌 (roll <= 1000)
res = system.gut_live_fish(inv, 2, 500)
_assert(res["ok"] == true, "Found clam inside fish!")
_assert(res["result"] == "clam", "Result is clam")
_assert(res["vnum"] == 27987, "Spawned Clam (27987)")
# 4. 产生蚯蚓 (roll 1001~1500)
res = system.gut_live_fish(inv, 3, 1200)
_assert(res["ok"] == true, "Found earthworm")
_assert(res["result"] == "earthworm", "Result is earthworm")
_assert(res["vnum"] == 27801, "Spawned Earthworm (27801)")
# 5. 滑入水中溜走 (roll > 1500)
res = system.gut_live_fish(inv, 4, 1800)
_assert(res["ok"] == true, "Escaped")
_assert(res["result"] == "escape", "Fish escaped back to water")
func test_clam_pearl_opening() -> void:
print("\n--- Test 2: Clam Opening & 3 Pearls (40250 1:1) ---")
var system = FishClamRoastSystem.new()
var inv: Array = [
{"vnum": 27987, "count": 1}, # 蚌 1: 碎石
{"vnum": 27987, "count": 1}, # 蚌 2: 空
{"vnum": 27987, "count": 1}, # 蚌 3: 白珍珠
{"vnum": 27987, "count": 1}, # 蚌 4: 蓝珍珠
{"vnum": 27987, "count": 1} # 蚌 5: 血红珍珠
]
# 1. 碎石 (branch roll <= 50)
var res = system.open_clam(inv, 0, 30)
_assert(res["ok"] == true, "Opened clam for stone")
_assert(res["result"] == "stone", "Result is stone")
_assert(res["vnum"] == 27990, "Vnum is 27990 (Stone Piece)")
# 2. 空蚌 (branch roll > 50, pearl roll <= 80)
res = system.open_clam(inv, 1, 70, 50)
_assert(res["ok"] == true, "Opened empty clam")
_assert(res["result"] == "empty", "Result is empty")
# 3. 白珍珠 (pearl roll 81~90)
res = system.open_clam(inv, 2, 70, 85)
_assert(res["ok"] == true, "Opened White Pearl")
_assert(res["result"] == "white_pearl", "Result is white_pearl")
_assert(res["vnum"] == 27992, "Vnum is 27992 (White Pearl)")
# 4. 蓝珍珠 (pearl roll 91~97)
res = system.open_clam(inv, 3, 70, 95)
_assert(res["ok"] == true, "Opened Blue Pearl")
_assert(res["result"] == "blue_pearl", "Result is blue_pearl")
_assert(res["vnum"] == 27993, "Vnum is 27993 (Blue Pearl)")
# 5. 血红珍珠 (pearl roll >= 98)
res = system.open_clam(inv, 4, 70, 99)
_assert(res["ok"] == true, "Opened Blood Pearl")
_assert(res["result"] == "blood_pearl", "Result is blood_pearl")
_assert(res["vnum"] == 27994, "Vnum is 27994 (Blood Pearl)")
func test_campfire_grilling() -> void:
print("\n--- Test 3: Campfire Grilling (40250 1:1) ---")
var system = FishClamRoastSystem.new()
var inv: Array = [
{"vnum": 27836, "count": 3}, # 3 条死鲤鱼
{"vnum": 27838, "count": 1} # 1 条死草鱼
]
# 未靠近营火拒绝烤制
var res = system.grill_fish(inv, 0, false)
_assert(res["ok"] == false, "Rejected grilling without campfire")
_assert(res["reason"] == "NO_CAMPFIRE", "Reason is NO_CAMPFIRE")
# 点燃营火
system.place_campfire(600.0)
_assert(system.is_near_active_campfire == true, "Campfire is active")
# 烤制 3 条死鲤鱼 (27836 -> 27866 烤鲤鱼)
res = system.grill_fish(inv, 0)
_assert(res["ok"] == true, "Grilling Sazan succeeded")
_assert(res["cooked_vnum"] == 27866, "Cooked Sazan vnum is 27866")
_assert(res["count"] == 3, "Preserved count 3")
_assert(inv[0] != null and inv[0]["vnum"] == 27866 and inv[0]["count"] == 3, "Slot holds 3 cooked fish")
func test_cooked_fish_buffs() -> void:
print("\n--- Test 4: Cooked Fish Consumption & Buffs (40250 1:1) ---")
var system = FishClamRoastSystem.new()
var inv: Array = [
{"vnum": 27866, "count": 1}, # 烤鲤鱼: 移速 +20 (600s)
{"vnum": 27868, "count": 1}, # 烤草鱼: 攻速 +20 (600s)
{"vnum": 27877, "count": 1}, # 烤泥鳅: 隐身 (300s)
{"vnum": 27874, "count": 1}, # 烤海鲈鱼: 治愈中毒
{"vnum": 27863, "count": 1} # 烤鲈鱼: 恢复 180 HP
]
# 1. 食用烤鲤鱼
var res = system.eat_cooked_fish(inv, 0)
_assert(res["ok"] == true, "Ate cooked Sazan")
_assert(res["effect"] == "move_speed", "Effect is move_speed")
_assert(res["value"] == 20, "Value is 20")
_assert(res["duration"] == 600.0, "Duration is 600s")
# 2. 食用烤草鱼
res = system.eat_cooked_fish(inv, 1)
_assert(res["ok"] == true, "Ate cooked Ot Sazani")
_assert(res["effect"] == "attack_speed", "Effect is attack_speed")
_assert(res["value"] == 20, "Value is 20")
# 3. 食用烤泥鳅
res = system.eat_cooked_fish(inv, 2)
_assert(res["ok"] == true, "Ate cooked Copra")
_assert(res["effect"] == "invisibility", "Effect is invisibility")
_assert(res["duration"] == 300.0, "Duration is 300s")
# 4. 食用解毒鱼与瞬间恢复鱼
res = system.eat_cooked_fish(inv, 3)
_assert(res["effect"] == "cure_poison", "Effect is cure_poison")
res = system.eat_cooked_fish(inv, 4)
_assert(res["effect"] == "instant_hp" and res["value"] == 180, "Instant HP restored 180")
# 检查增益状态列表
var buffs = system.get_active_buffs()
_assert(buffs.has("move_speed"), "Active buff has move_speed")
_assert(buffs.has("attack_speed"), "Active buff has attack_speed")
_assert(buffs.has("invisibility"), "Active buff has invisibility")
# 时间推移 100 秒
system.update(100.0)
_assert(buffs["invisibility"]["time_remaining"] <= 200.0, "Invisibility ticked down")
# 时间推移 250 秒 (隐身 300s 到期清除)
system.update(250.0)
_assert(not system.get_active_buffs().has("invisibility"), "Invisibility expired and removed")
_assert(system.get_active_buffs().has("move_speed"), "Move speed still active")
func test_serialization() -> void:
print("\n--- Test 5: Buff Serialization & Restoration ---")
var system = FishClamRoastSystem.new()
system.place_campfire(500.0)
var inv: Array = [{"vnum": 27866, "count": 1}]
system.eat_cooked_fish(inv, 0)
var data = system.serialize()
_assert(data.has("active_buffs") and data["active_buffs"].has("move_speed"), "Serialized move_speed buff")
var restored = FishClamRoastSystem.new()
restored.deserialize(data)
_assert(restored.is_near_active_campfire == true, "Restored active campfire")
_assert(restored.get_active_buffs().has("move_speed"), "Restored move_speed buff")