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
+138
View File
@@ -0,0 +1,138 @@
# horse_system.gd —— 40250 战马与骑乘系统 1:1 规则引擎
# 严格对照:
# metin2/src/server/game/src/cmd_general.cpp:45-100 (do_user_horse_ride / back / feed)
# metin2/src/server/game/src/cmd_general.cpp:2426-2465 (do_ride)
# root/game.py:344, 461-490 (Ctrl+G, Ctrl+H, Ctrl+B, Ctrl+F)
# ClientVS22/UserInterface/InstanceBase.cpp (IsMountingHorse, 5.0m attack range)
extends RefCounted
const ITEM_HORSE_PICTURE := 50051 # 初级马画 (Lv 1 - 10)
const ITEM_HORSE_MEDAL := 50052 # 战斗马牌 (Lv 11 - 20)
const ITEM_HORSE_MILITARY := 50053 # 军马牌 (Lv 21 - 30)
const ITEM_FOOD_HAY := 50057 # 干草
const ITEM_FOOD_CARROT := 50054 # 胡萝卜
const ITEM_FOOD_GINSENG := 50058 # 人参
const HORSE_TIER_NONE := 0
const HORSE_TIER_BEGINNER := 1 # 普通马
const HORSE_TIER_COMBAT := 2 # 战斗马(可马上砍怪)
const HORSE_TIER_MILITARY := 3 # 军马(可使用马术技能)
# 召唤消耗 SP
const SUMMON_SP_COST := {
HORSE_TIER_BEGINNER: 100,
HORSE_TIER_COMBAT: 200,
HORSE_TIER_MILITARY: 300,
}
# 基础召唤成功率
const BASE_SUMMON_CHANCE := {
HORSE_TIER_BEGINNER: 10,
HORSE_TIER_COMBAT: 20,
HORSE_TIER_MILITARY: 30,
}
# 骑乘时战斗与拾取距离加成(从 300cm 扩至 500cm1:1 ClientVS22
const WALKING_ATTACK_RANGE := 3.0
const MOUNTED_ATTACK_RANGE := 5.0
# 判定马牌对应的级别与需求
static func get_horse_tier_from_item(vnum: int) -> int:
match vnum:
ITEM_HORSE_PICTURE: return HORSE_TIER_BEGINNER
ITEM_HORSE_MEDAL: return HORSE_TIER_COMBAT
ITEM_HORSE_MILITARY: return HORSE_TIER_MILITARY
_: return HORSE_TIER_NONE
# 所需饲料
static func get_required_food(tier: int) -> int:
match tier:
HORSE_TIER_BEGINNER: return ITEM_FOOD_HAY
HORSE_TIER_COMBAT: return ITEM_FOOD_CARROT
HORSE_TIER_MILITARY: return ITEM_FOOD_GINSENG
_: return 0
# 召唤几率计算(1:1 char_horse.cpp: HorseSummon
# chance = base_chance + horse_summon_skill_level * 10, max 100
static func calculate_summon_chance(tier: int, summon_skill_level: int) -> int:
var base: int = BASE_SUMMON_CHANCE.get(tier, 10)
return mini(100, base + summon_skill_level * 10)
# 执行召唤判定
static func try_summon_horse(
tier: int,
current_sp: int,
summon_skill_level: int,
roll_val := -1
) -> Dictionary:
var sp_cost: int = SUMMON_SP_COST.get(tier, 100)
if current_sp < sp_cost:
return {"success": false, "err": "insufficient_sp", "sp_cost": sp_cost}
var chance := calculate_summon_chance(tier, summon_skill_level)
var dice := roll_val if roll_val > 0 else (randi() % 100 + 1)
var success := (dice <= chance)
return {
"success": success,
"sp_cost": sp_cost,
"tier": tier,
"err": "" if success else "summon_failed",
}
# 执行 /user_horse_ride (Ctrl+H) 状态机 (cmd_general.cpp:45)
static func handle_user_horse_ride(
is_riding: bool,
is_horse_summoned: bool,
is_other_mount: bool,
is_dead_or_stun: bool
) -> Dictionary:
if is_dead_or_stun:
return {"action": "none", "err": "dead_or_stun"}
if not is_riding:
if is_other_mount:
return {"action": "none", "err": "already_mounting_other", "msg": "已经在使用其他坐骑。"}
if not is_horse_summoned:
return {"action": "none", "err": "horse_not_summoned", "msg": "请先召唤马匹。"}
return {"action": "start_riding", "err": "", "is_riding": true}
else:
return {"action": "stop_riding", "err": "", "is_riding": false}
# 执行 /user_horse_back (Ctrl+B) 状态机 (cmd_general.cpp:75)
static func handle_user_horse_back(
is_riding: bool,
is_horse_summoned: bool
) -> Dictionary:
if is_riding:
return {"action": "none", "err": "must_dismount_first", "msg": "请先下马。"}
if not is_horse_summoned:
return {"action": "none", "err": "horse_not_summoned", "msg": "请先召唤马匹。"}
return {"action": "unsummon", "err": "", "is_horse_summoned": false, "msg": "已送回马匹。"}
# 执行 /ride (Ctrl+G) 状态机 (cmd_general.cpp:2426)
static func handle_ride(
is_riding: bool,
is_horse_summoned: bool,
has_ride_item: bool
) -> Dictionary:
if is_riding:
return {"action": "stop_riding", "err": "", "is_riding": false}
if is_horse_summoned:
return {"action": "start_riding", "err": "", "is_riding": true}
if has_ride_item:
return {"action": "mount_item", "err": "", "is_riding": true}
return {"action": "none", "err": "cannot_ride", "msg": "没有可骑乘的马匹或坐骑。"}
# 喂食逻辑 (Ctrl+F)
static func handle_user_horse_feed(
tier: int,
food_item_vnum: int
) -> Dictionary:
var req_food := get_required_food(tier)
if req_food == 0:
return {"success": false, "msg": "当前没有出战马匹。"}
if food_item_vnum != req_food:
return {"success": false, "msg": "这种食物不适合当前马匹。"}
return {"success": true, "consumed_vnum": req_food, "msg": "战马饱餐了一顿,恢复了活力!"}