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
+224
View File
@@ -0,0 +1,224 @@
# mounted_combat_skill_system.gd —— Metin2 40250 野外军马冲杀与马上技能协同打击 1:1
# 对照 40250 服务端 char_horse.cpp, char_battle.cpp, skill.cpp
class_name MountedCombatSkillSystem
extends RefCounted
signal horse_mounted(tier: int, horse_level: int, speed_bonus: int)
signal horse_unmounted()
signal mounted_attack_executed(target_count: int, total_dmg: int)
signal horse_skill_cast(skill_vnum: int, skill_name: String, dmg: int, hit_count: int)
signal horse_fed(item_name: String, recovered_stamina: int)
enum HorseTier {
NONE = 0,
NORMAL = 1, # 1~10 级 (代步)
COMBAT = 2, # 11~20 级 (马上普攻)
MILITARY = 3 # 21~30 级 (马上 3 大绝技)
}
const SKILL_HORSE_CHARGE := 121 # 马上突刺 (Horse Charge)
const SKILL_HORSE_STOMP := 122 # 踏马震地 (Horse Stomp)
const SKILL_HORSE_WAVE := 123 # 马尾狂风 (Horse Wave)
const HORSE_SKILLS: Dictionary = {
SKILL_HORSE_CHARGE: {
"name": "马背突刺",
"mult": 2.5,
"cd": 12.0,
"sp": 60,
"range": 12.0,
"desc": "战马极速直线突进贯穿群怪,造成击飞与重创"
},
SKILL_HORSE_STOMP: {
"name": "踏马震地",
"mult": 2.0,
"cd": 15.0,
"sp": 75,
"range": 6.0,
"desc": "战马双蹄扬起践踏大地,造成 360 度范围伤害与强力眩晕"
},
SKILL_HORSE_WAVE: {
"name": "马尾狂风",
"mult": 1.8,
"cd": 10.0,
"sp": 50,
"range": 5.0,
"desc": "战马回旋摆尾卷起狂风,大范围震退近身敌群"
}
}
const FOOD_ITEMS: Dictionary = {
50054: {"name": "干草", "stamina": 30},
50055: {"name": "胡萝卜", "stamina": 60},
50056: {"name": "红参", "stamina": 100}
}
var is_mounted: bool = false
var horse_level: int = 1
var horse_tier: int = HorseTier.NONE
var speed_bonus: int = 0
var stamina: int = 100
var max_stamina: int = 100
var skill_cooldowns: Dictionary = {
SKILL_HORSE_CHARGE: 0.0,
SKILL_HORSE_STOMP: 0.0,
SKILL_HORSE_WAVE: 0.0
}
# 骑乘战马
func mount_horse(level: int = 21) -> Dictionary:
if is_mounted:
return {"ok": false, "reason": "ALREADY_MOUNTED"}
horse_level = clamp(level, 1, 30)
if horse_level >= 21:
horse_tier = HorseTier.MILITARY
speed_bonus = 50
elif horse_level >= 11:
horse_tier = HorseTier.COMBAT
speed_bonus = 40
else:
horse_tier = HorseTier.NORMAL
speed_bonus = 30
is_mounted = true
horse_mounted.emit(horse_tier, horse_level, speed_bonus)
return {
"ok": true,
"tier": horse_tier,
"level": horse_level,
"speed_bonus": speed_bonus,
"msg": "跨上战马!移动速度提升 %d 点。" % speed_bonus
}
# 下马
func unmount_horse() -> Dictionary:
if not is_mounted:
return {"ok": false, "reason": "NOT_MOUNTED"}
is_mounted = false
horse_tier = HorseTier.NONE
speed_bonus = 0
horse_unmounted.emit()
return {"ok": true, "msg": "翻身下马。"}
# 心跳刷新技能冷却
func update(delta: float) -> void:
for sk in skill_cooldowns.keys():
if skill_cooldowns[sk] > 0.0:
skill_cooldowns[sk] = max(0.0, skill_cooldowns[sk] - delta)
# 马背普通连斩反击 (需战斗马以上)
func execute_mounted_attack(base_atk: int, targets: Array) -> Dictionary:
if not is_mounted:
return {"ok": false, "reason": "NOT_MOUNTED"}
if horse_tier < HorseTier.COMBAT:
return {"ok": false, "reason": "TIER_TOO_LOW", "msg": "普通马匹无法在马背上挥砍!需升级至 11 级战斗战马。"}
if stamina <= 0:
return {"ok": false, "reason": "EXHAUSTED", "msg": "战马体力耗尽,无法施展马战挥砍!请喂食干草或胡萝卜。"}
# 每次攻击消耗 1 点马匹耐力
stamina = max(0, stamina - 1)
var hit_count = 0
var total_dmg = 0
# 战马提供 1.2x 基础冲击加成
var dmg_per_hit = int(float(base_atk) * 1.2)
for tgt in targets:
hit_count += 1
total_dmg += dmg_per_hit
mounted_attack_executed.emit(hit_count, total_dmg)
return {
"ok": true,
"hit_count": hit_count,
"dmg_per_hit": dmg_per_hit,
"total_damage": total_dmg,
"stamina": stamina
}
# 释放马上专属技能 (需军用武装战马)
func cast_horse_skill(skill_vnum: int, base_atk: int, player_data: Dictionary, targets: Array) -> Dictionary:
if not is_mounted:
return {"ok": false, "reason": "NOT_MOUNTED"}
if horse_tier < HorseTier.MILITARY:
return {"ok": false, "reason": "TIER_TOO_LOW", "msg": "只有 21 级以上的【军用战马】才能释放马背专属技能!"}
if not HORSE_SKILLS.has(skill_vnum):
return {"ok": false, "reason": "INVALID_HORSE_SKILL"}
var cfg = HORSE_SKILLS[skill_vnum]
var cd: float = skill_cooldowns[skill_vnum]
if cd > 0.0:
return {"ok": false, "reason": "SKILL_ON_COOLDOWN", "msg": "技能正在冷却中!剩余 %.1f" % cd}
var sp_cost: int = cfg["sp"]
var cur_sp: int = int(player_data.get("sp", 0))
if cur_sp < sp_cost:
return {"ok": false, "reason": "NOT_ENOUGH_SP", "msg": "内力法力不足!需要 %d 点 SP" % sp_cost}
if stamina < 5:
return {"ok": false, "reason": "NOT_ENOUGH_STAMINA", "msg": "战马体力不足,无法施展大招!"}
# 消耗资源
player_data["sp"] = cur_sp - sp_cost
stamina = max(0, stamina - 5)
skill_cooldowns[skill_vnum] = cfg["cd"]
var skill_dmg = int(float(base_atk) * cfg["mult"])
var hit_count = targets.size()
horse_skill_cast.emit(skill_vnum, cfg["name"], skill_dmg, hit_count)
return {
"ok": true,
"skill_vnum": skill_vnum,
"skill_name": cfg["name"],
"damage": skill_dmg,
"hit_count": hit_count,
"stamina": stamina,
"remaining_sp": player_data["sp"],
"msg": "战马嘶鸣!施展了马上绝技【%s】!" % cfg["name"]
}
# 喂食战马恢复体力
func feed_horse(food_vnum: int, inventory: Array) -> Dictionary:
if not FOOD_ITEMS.has(food_vnum):
return {"ok": false, "reason": "INVALID_FOOD"}
var slot = -1
for i in range(inventory.size()):
if inventory[i] != null and int(inventory[i].get("vnum", 0)) == food_vnum:
slot = i
break
if slot == -1:
return {"ok": false, "reason": "FOOD_NOT_IN_INVENTORY", "msg": "背包中没有此马粮!"}
var item = inventory[slot]
var cnt = int(item.get("count", 1))
if cnt > 1:
item["count"] = cnt - 1
else:
inventory[slot] = null
var food = FOOD_ITEMS[food_vnum]
var recovered = food["stamina"]
stamina = min(max_stamina, stamina + recovered)
horse_fed.emit(food["name"], recovered)
return {
"ok": true,
"food_name": food["name"],
"recovered": recovered,
"current_stamina": stamina,
"msg": "喂食了【%s】,战马体力恢复至 %d/%d" % [food["name"], stamina, max_stamina]
}