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
+218
View File
@@ -0,0 +1,218 @@
# consumable_system.gd —— 高级消耗品与瞬移定位系统(40250 官方 1:1 对齐)
# 对齐源码:
# - 40250 char_item.cpp:3950-4050 (AFFECT_AUTO_HP/SP_RECOVERY, 日月神水开关)
# - 40250 char_item.cpp:7326-7390 (AutoRecoveryItemProcess, 自动补充与容量耗尽)
# - 40250 affect.h:95-96 (AFFECT_AUTO_HP_RECOVERY = 534, AFFECT_AUTO_SP_RECOVERY = 535)
# - 40250 char_item.cpp 龙神药水 (71027~71030: 攻/防/血/魔法比率加成, 30 分钟)
# - 40250 char_item.cpp 卷轴 (22001 回城卷轴, 22010 地标传送卷轴)
class_name ConsumableSystem
extends RefCounted
# 日月神水 Vnum 范围(40250 官方标准)
const VNUM_SUN_ELIXIR_S := 72723 # 太阳神水 (小) - 1,000,000 HP
const VNUM_SUN_ELIXIR_M := 72724 # 太阳神水 (中) - 3,000,000 HP
const VNUM_SUN_ELIXIR_L := 72725 # 太阳神水 (大) - 5,000,000 HP
const VNUM_SUN_ELIXIR_EX := 72726 # 太阳神水 (特别) - 10,000,000 HP
const VNUM_MOON_ELIXIR_S := 72727 # 月亮神水 (小) - 100,000 SP
const VNUM_MOON_ELIXIR_M := 72728 # 月亮神水 (中) - 300,000 SP
const VNUM_MOON_ELIXIR_L := 72729 # 月亮神水 (大) - 500,000 SP
const VNUM_MOON_ELIXIR_EX := 72730 # 月亮神水 (特别) - 1,000,000 SP
# 龙神药水 Vnum
const VNUM_DRAGON_GOD_ATTACK := 71027 # 龙神攻击 (+15% 伤害)
const VNUM_DRAGON_GOD_DEFENSE := 71028 # 龙神防御 (+15% 防御)
const VNUM_DRAGON_GOD_LIFE := 71029 # 龙神生命 (+20% HP)
const VNUM_DRAGON_GOD_MANA := 71030 # 龙神法力 (+20% SP)
# 卷轴 Vnum
const VNUM_TOWN_SCROLL := 22001 # 瞬间回城卷轴
const VNUM_RETURN_SCROLL := 22010 # 地标记忆/传送卷轴
# 40250 Affect 编号
const AFFECT_AUTO_HP_RECOVERY := 534
const AFFECT_AUTO_SP_RECOVERY := 535
const DRAGON_GOD_DURATION := 1800.0 # 30 分钟持续时间
# 神水默认容量表 (Vnum -> 容量)
const ELIXIR_CAPACITIES := {
72723: 1000000,
72724: 3000000,
72725: 5000000,
72726: 10000000,
72727: 100000,
72728: 300000,
72729: 500000,
72730: 1000000,
}
## 检查是否为日月神水
static func is_elixir(vnum: int) -> bool:
return (vnum >= 72723 and vnum <= 72730)
## 检查是否为生命神水(太阳神水)
static func is_hp_elixir(vnum: int) -> bool:
return (vnum >= 72723 and vnum <= 72726)
## 检查是否为法力神水(月亮神水)
static func is_sp_elixir(vnum: int) -> bool:
return (vnum >= 72727 and vnum <= 72730)
## 检查是否为龙神特药
static func is_dragon_god_potion(vnum: int) -> bool:
return vnum in [VNUM_DRAGON_GOD_ATTACK, VNUM_DRAGON_GOD_DEFENSE, VNUM_DRAGON_GOD_LIFE, VNUM_DRAGON_GOD_MANA]
## 初始化神水数据(若插槽容量未设定)
static func init_elixir_if_needed(item_data: Dictionary) -> void:
var vnum := int(item_data.get("vnum", 0))
var sockets: Array = item_data.get("sockets", [0, 0, 0]).duplicate()
while sockets.size() < 3:
sockets.append(0)
# socket[0]: 0=未激活, 1=激活
# socket[1]: 已使用量
# socket[2]: 总容量
if int(sockets[2]) <= 0:
sockets[2] = ELIXIR_CAPACITIES.get(vnum, 1000000)
sockets[0] = 0
sockets[1] = 0
item_data["sockets"] = sockets
## 切换神水激活状态 (40250 char_item.cpp:3950)
static func toggle_elixir(item_data: Dictionary) -> Dictionary:
init_elixir_if_needed(item_data)
var sockets: Array = item_data["sockets"]
var cur_active := (int(sockets[0]) != 0)
var new_active := not cur_active
sockets[0] = 1 if new_active else 0
item_data["sockets"] = sockets
var is_hp := is_hp_elixir(int(item_data.get("vnum", 0)))
var kind := "生命" if is_hp else "法力"
var msg := ("已激活%s神水自充能!" % kind) if new_active else ("已关闭%s神水自充能。" % kind)
return {
"ok": true,
"active": new_active,
"affect_type": AFFECT_AUTO_HP_RECOVERY if is_hp else AFFECT_AUTO_SP_RECOVERY,
"msg": msg,
}
## 自充能 Tick 处理 (40250 char_item.cpp:7326 AutoRecoveryItemProcess)
## 返回消耗与恢复量,若耗尽返回 exhausted=true
static func process_elixir_recovery(item_data: Dictionary, cur_val: int, max_val: int) -> Dictionary:
init_elixir_if_needed(item_data)
var sockets: Array = item_data["sockets"]
if int(sockets[0]) == 0:
return {"recovered": 0, "exhausted": false} # 未激活
var used := int(sockets[1])
var cap := int(sockets[2])
var avail := maxi(0, cap - used)
if avail <= 0:
return {"recovered": 0, "exhausted": true}
var deficit := maxi(0, max_val - cur_val)
if deficit <= 0:
return {"recovered": 0, "exhausted": false}
var heal: int = mini(deficit, avail)
sockets[1] = used + heal
item_data["sockets"] = sockets
var is_depleted := (int(sockets[1]) >= cap)
return {
"recovered": heal,
"exhausted": is_depleted,
"remaining": maxi(0, cap - int(sockets[1])),
"capacity": cap,
}
## 使用龙神特药 (40250 char_item.cpp)
## 返回 Buff 增益配置
static func use_dragon_god_potion(vnum: int) -> Dictionary:
match vnum:
VNUM_DRAGON_GOD_ATTACK:
return {
"ok": true,
"vnum": vnum,
"name": "龙神攻击",
"duration": DRAGON_GOD_DURATION,
"bonus_type": "POINT_ATT_BONUS",
"bonus_value": 15,
"msg": "使用了龙神攻击,攻击力提升 15%,持续 30 分钟!"
}
VNUM_DRAGON_GOD_DEFENSE:
return {
"ok": true,
"vnum": vnum,
"name": "龙神防御",
"duration": DRAGON_GOD_DURATION,
"bonus_type": "POINT_DEF_BONUS",
"bonus_value": 15,
"msg": "使用了龙神防御,防御力提升 15%,持续 30 分钟!"
}
VNUM_DRAGON_GOD_LIFE:
return {
"ok": true,
"vnum": vnum,
"name": "龙神生命",
"duration": DRAGON_GOD_DURATION,
"bonus_type": "POINT_MAX_HP_PCT",
"bonus_value": 20,
"msg": "使用了龙神生命,最大生命值提升 20%,持续 30 分钟!"
}
VNUM_DRAGON_GOD_MANA:
return {
"ok": true,
"vnum": vnum,
"name": "龙神法力",
"duration": DRAGON_GOD_DURATION,
"bonus_type": "POINT_MAX_SP_PCT",
"bonus_value": 20,
"msg": "使用了龙神法力,最大法力值提升 20%,持续 30 分钟!"
}
return {"ok": false, "msg": "未知的龙神药水"}
## 使用回城卷轴 / 地标卷轴 (40250 char_item.cpp)
static func use_warp_scroll(vnum: int, item_data: Dictionary, cur_x: float, cur_y: float, cur_map_id := 1) -> Dictionary:
if vnum == VNUM_TOWN_SCROLL:
return {
"ok": true,
"action": "TOWN_WARP",
"consumed": true,
"msg": "使用了回城卷轴,即将传送回城!"
}
elif vnum == VNUM_RETURN_SCROLL:
var sockets: Array = item_data.get("sockets", [0, 0, 0]).duplicate()
while sockets.size() < 3:
sockets.append(0)
# 若未记录坐标(socket[0] == 0),则记录当前位置,不消耗卷轴!
if int(sockets[0]) == 0 and int(sockets[1]) == 0:
sockets[0] = int(cur_x)
sockets[1] = int(cur_y)
sockets[2] = cur_map_id
item_data["sockets"] = sockets
return {
"ok": true,
"action": "RECORDED",
"consumed": false,
"coords": Vector2(cur_x, cur_y),
"map_id": cur_map_id,
"msg": "成功将当前坐标 (%d, %d) 记录到地标卷轴!" % [int(cur_x), int(cur_y)]
}
else:
# 已记录坐标:传送至记录位置并消耗卷轴
var target_x := int(sockets[0])
var target_y := int(sockets[1])
var target_map := int(sockets[2])
return {
"ok": true,
"action": "WARP_TO_COORDS",
"consumed": true,
"target_x": target_x,
"target_y": target_y,
"target_map": target_map,
"msg": "使用了地标卷轴,正在传送至 (%d, %d)" % [target_x, target_y]
}
return {"ok": false, "msg": "未知的传送卷轴"}