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
+228
View File
@@ -0,0 +1,228 @@
# herbal_dew_system.gd —— Metin2 40250 白高草药炼丹酿造与 6 色神圣露水系统 1:1
# 对照 40250 服务端 char_item.cpp, blend.txt, potion.quest 及 NPC 20018 Baek-Go
class_name HerbalDewSystem
extends RefCounted
const NPC_BAEK_GO := 20018
const ITEM_EMPTY_BOTTLE := 70030
# 6 色神圣露水定义 (50821~50826)
const DEW_TABLE: Dictionary = {
50821: {
"name": "绿露水",
"color": "green",
"stat": "magic_defense",
"stat_desc": "魔法防御力",
"min_val": 30,
"max_val": 70,
"duration": 600.0, # 10 分钟
"water_vnum": 50721
},
50822: {
"name": "红露水",
"color": "red",
"stat": "critical_pct",
"stat_desc": "致命一击几率",
"min_val": 8,
"max_val": 20,
"duration": 600.0,
"water_vnum": 50722
},
50823: {
"name": "蓝露水",
"color": "blue",
"stat": "att_grade",
"stat_desc": "物理攻击力",
"min_val": 50,
"max_val": 120,
"duration": 600.0,
"water_vnum": 50723
},
50824: {
"name": "白露水",
"color": "white",
"stat": "def_grade",
"stat_desc": "物理防御力",
"min_val": 40,
"max_val": 100,
"duration": 600.0,
"water_vnum": 50724
},
50825: {
"name": "粉露水",
"color": "pink",
"stat": "penetrate_pct",
"stat_desc": "穿透一击几率",
"min_val": 8,
"max_val": 20,
"duration": 600.0,
"water_vnum": 50725
},
50826: {
"name": "黄露水",
"color": "yellow",
"stat": "att_speed",
"stat_desc": "攻击速度",
"min_val": 2,
"max_val": 8,
"duration": 600.0,
"water_vnum": 50726
}
}
# 草药提炼对应表 (herb_vnum -> water_vnum)
const HERB_TO_WATER: Dictionary = {
50701: 50721, # 桃花 -> Sim-Water
50702: 50722, # 风铃草 -> Dok-Water
50703: 50723, # 柿子花 -> Bo-Water
50704: 50724, # 高山石斛 -> Young-Water
50705: 50725, # 紫丁香 -> Zin-Water
50708: 50726 # 桑椹 -> Sam-Water
}
# 正在生效的露水增益: dew_vnum -> { name, stat, value, duration }
var active_dews: Dictionary = {}
# 1. 在白高处提取草药药水 (原料草药 -> 中间提取液)
static func brew_water(inventory: Array, herb_vnum: int) -> Dictionary:
if not HERB_TO_WATER.has(herb_vnum):
return {"ok": false, "reason": "INVALID_HERB", "msg": "此草药无法用于提炼!"}
var target_water: int = int(HERB_TO_WATER[herb_vnum])
# 消耗草药
var herb_slot := -1
for i in range(inventory.size()):
var it = inventory[i]
if it != null and int(it.get("vnum", 0)) == herb_vnum:
herb_slot = i
break
if herb_slot == -1:
return {"ok": false, "reason": "MISSING_HERB", "msg": "背包中缺少所需草药!"}
inventory[herb_slot] = null
# 寻找背包空位生成药水提取液
var empty_slot := -1
for i in range(inventory.size()):
if inventory[i] == null:
empty_slot = i
break
if empty_slot == -1:
return {"ok": false, "reason": "INVENTORY_FULL", "msg": "背包已满!"}
inventory[empty_slot] = {"vnum": target_water, "count": 1}
return {"ok": true, "water_vnum": target_water, "slot": empty_slot}
# 2. 在白高处合成露水 (药水提取液 + 空瓶 70030 -> 神圣露水)
static func brew_dew(inventory: Array, dew_vnum: int, force_max_roll: bool = false) -> Dictionary:
if not DEW_TABLE.has(dew_vnum):
return {"ok": false, "reason": "INVALID_DEW"}
var cfg: Dictionary = DEW_TABLE[dew_vnum]
var req_water: int = int(cfg["water_vnum"])
var water_slot := -1
var bottle_slot := -1
for i in range(inventory.size()):
var it = inventory[i]
if it != null:
var v: int = int(it.get("vnum", 0))
if v == req_water and water_slot == -1:
water_slot = i
elif v == ITEM_EMPTY_BOTTLE and bottle_slot == -1:
bottle_slot = i
if water_slot == -1:
return {"ok": false, "reason": "MISSING_WATER", "msg": "缺少对应的草药提取水剂!"}
if bottle_slot == -1:
return {"ok": false, "reason": "MISSING_BOTTLE", "msg": "缺少用于盛装露水的空药瓶 (70030)!"}
# 扣除水剂与空药瓶
inventory[water_slot] = null
inventory[bottle_slot] = null
# 抽取露水品质数值 (min_val ~ max_val)
var min_v: int = int(cfg["min_val"])
var max_v: int = int(cfg["max_val"])
var roll_val: int = max_v if force_max_roll else randi_range(min_v, max_v)
var dew_item: Dictionary = {
"vnum": dew_vnum,
"name": cfg["name"],
"value": roll_val,
"duration": float(cfg["duration"])
}
# 存入背包
var stored_slot := -1
for i in range(inventory.size()):
if inventory[i] == null:
inventory[i] = dew_item
stored_slot = i
break
return {
"ok": true,
"dew_vnum": dew_vnum,
"name": cfg["name"],
"value": roll_val,
"slot": stored_slot,
"stat_desc": cfg["stat_desc"],
"msg": "成功酿造出【%s】!品质增幅:%s +%d" % [cfg["name"], cfg["stat_desc"], roll_val]
}
# 3. 饮用露水获得增益
func drink_dew(item: Dictionary) -> Dictionary:
var vnum: int = int(item.get("vnum", 0))
if not DEW_TABLE.has(vnum):
return {"ok": false, "reason": "NOT_A_DEW"}
var cfg: Dictionary = DEW_TABLE[vnum]
var roll_val: int = int(item.get("value", cfg["min_val"]))
active_dews[vnum] = {
"name": cfg["name"],
"stat": cfg["stat"],
"value": roll_val,
"duration": float(cfg["duration"]),
"total_duration": float(cfg["duration"])
}
return {
"ok": true,
"dew_vnum": vnum,
"name": cfg["name"],
"stat": cfg["stat"],
"value": roll_val,
"duration": float(cfg["duration"])
}
# 逐帧更新倒计时
func update(dt: float) -> Array[int]:
var expired: Array[int] = []
for vnum in active_dews.keys():
var data: Dictionary = active_dews[vnum]
data["duration"] = maxf(0.0, float(data["duration"]) - dt)
if float(data["duration"]) <= 0.0:
expired.append(vnum)
for vnum in expired:
active_dews.erase(vnum)
return expired
# 查询某项属性在当前露水中的总加成
func get_stat_bonus(stat_name: String) -> int:
var total := 0
for vnum in active_dews:
var data: Dictionary = active_dews[vnum]
if String(data.get("stat", "")) == stat_name:
total += int(data.get("value", 0))
return total
func has_dew(vnum: int) -> bool:
return active_dews.has(vnum)