- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
225 lines
6.3 KiB
GDScript
225 lines
6.3 KiB
GDScript
# 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]
|
||
}
|