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:
@@ -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]
|
||||
}
|
||||
Reference in New Issue
Block a user