- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
208 lines
5.8 KiB
GDScript
208 lines
5.8 KiB
GDScript
# horse_combat_system.gd —— Metin2 40250 战马骑乘战斗与 4 大战马专属技能 1:1
|
|
# 对照 40250 服务端 char_skill.cpp, char_horse.cpp, skilldesc.txt:137-140
|
|
class_name HorseCombatSystem
|
|
extends RefCounted
|
|
|
|
signal horse_attack_performed(target_count: int, total_dmg: int)
|
|
signal horse_skill_used(skill_id: int, skill_name: String, cost_sp: int)
|
|
signal horse_stamina_changed(current: float, max_val: float)
|
|
|
|
const HORSE_TIER_BEGINNER := 1 # 初级马 (Lv 1-10, 仅代步)
|
|
const HORSE_TIER_COMBAT := 2 # 战斗马 (Lv 11-20, 可马上挥砍)
|
|
const HORSE_TIER_MILITARY := 3 # 军马 (Lv 21-30, 可马上挥砍并施展 4 大马术技能)
|
|
|
|
const MOUNTED_ATTACK_RANGE := 5.0
|
|
const FOOT_ATTACK_RANGE := 3.0
|
|
|
|
const ITEM_FOOD_CARROT := 50054 # 胡萝卜 (战斗马饲料)
|
|
const ITEM_FOOD_GINSENG := 50058 # 人参 (军马饲料)
|
|
|
|
const HORSE_SKILLS: Dictionary = {
|
|
137: {
|
|
"name": "战马砍击",
|
|
"sp_cost": 30,
|
|
"cooldown": 15.0,
|
|
"dmg_mult": 2.5,
|
|
"effect": "KNOCKBACK",
|
|
"range": 5.0
|
|
},
|
|
138: {
|
|
"name": "战马突刺",
|
|
"sp_cost": 35,
|
|
"cooldown": 20.0,
|
|
"dmg_mult": 2.0,
|
|
"effect": "STUMBLE",
|
|
"range": 8.0
|
|
},
|
|
139: {
|
|
"name": "战马震击",
|
|
"sp_cost": 40,
|
|
"cooldown": 25.0,
|
|
"dmg_mult": 3.0,
|
|
"effect": "AOE_STUN",
|
|
"range": 6.0
|
|
},
|
|
140: {
|
|
"name": "破浪击",
|
|
"sp_cost": 30,
|
|
"cooldown": 12.0,
|
|
"dmg_mult": 2.2,
|
|
"effect": "RANGED_WAVE",
|
|
"range": 12.0
|
|
}
|
|
}
|
|
|
|
var is_riding: bool = false
|
|
var horse_tier: int = HORSE_TIER_NONE
|
|
var horse_stamina: float = 100.0
|
|
var max_stamina: float = 100.0
|
|
var skill_cooldowns: Dictionary = {
|
|
137: 0.0,
|
|
138: 0.0,
|
|
139: 0.0,
|
|
140: 0.0
|
|
}
|
|
|
|
const HORSE_TIER_NONE := 0
|
|
|
|
# 设置骑乘状态
|
|
func set_mounted_state(riding: bool, tier: int = HORSE_TIER_MILITARY) -> void:
|
|
is_riding = riding
|
|
horse_tier = tier if riding else HORSE_TIER_NONE
|
|
|
|
# 马上普通挥砍攻击 (Mounted Basic Sweep Attack)
|
|
func perform_mounted_basic_attack(
|
|
base_att: int,
|
|
targets_in_range: Array
|
|
) -> Dictionary:
|
|
if not is_riding:
|
|
return {"ok": false, "reason": "NOT_RIDING", "msg": "未处于骑乘状态!"}
|
|
|
|
if horse_tier < HORSE_TIER_COMBAT:
|
|
return {"ok": false, "reason": "HORSE_TIER_TOO_LOW", "msg": "当前马匹等级不足,无法在马上战斗!需要战斗马或军马。"}
|
|
|
|
# 精力衰减
|
|
horse_stamina = maxf(0.0, horse_stamina - 0.5)
|
|
horse_stamina_changed.emit(horse_stamina, max_stamina)
|
|
|
|
# 伤害计算:马上横扫增伤 1.2x (精力枯竭时惩罚为 0.8x)
|
|
var mult: float = 1.2 if horse_stamina > 0.0 else 0.8
|
|
var dmg_per_target := int(base_att * mult)
|
|
var total_dmg: int = 0
|
|
var hit_targets: Array = []
|
|
|
|
for target in targets_in_range:
|
|
var dist: float = float(target.get("distance", 999.0))
|
|
if dist <= MOUNTED_ATTACK_RANGE:
|
|
hit_targets.append(target)
|
|
total_dmg += dmg_per_target
|
|
|
|
horse_attack_performed.emit(hit_targets.size(), total_dmg)
|
|
|
|
return {
|
|
"ok": true,
|
|
"attack_range": MOUNTED_ATTACK_RANGE,
|
|
"hit_count": hit_targets.size(),
|
|
"damage_per_hit": dmg_per_target,
|
|
"total_damage": total_dmg,
|
|
"stamina": horse_stamina
|
|
}
|
|
|
|
# 施展 4 大战马专属技能 (137, 138, 139, 140)
|
|
func use_horse_skill(
|
|
skill_id: int,
|
|
player_sp: int,
|
|
base_att: int
|
|
) -> Dictionary:
|
|
if not is_riding:
|
|
return {"ok": false, "reason": "NOT_RIDING", "msg": "未在马背上,无法施展马术技能!"}
|
|
|
|
if horse_tier < HORSE_TIER_MILITARY:
|
|
return {"ok": false, "reason": "REQUIRES_MILITARY_HORSE", "msg": "马术专属技能需要军马 (Lv21+) 方可施展!"}
|
|
|
|
if not HORSE_SKILLS.has(skill_id):
|
|
return {"ok": false, "reason": "INVALID_SKILL_ID", "msg": "未知的战马技能!"}
|
|
|
|
var cfg: Dictionary = HORSE_SKILLS[skill_id]
|
|
|
|
# 检查冷却
|
|
if skill_cooldowns[skill_id] > 0.0:
|
|
return {
|
|
"ok": false,
|
|
"reason": "SKILL_ON_COOLDOWN",
|
|
"remaining_cd": skill_cooldowns[skill_id],
|
|
"msg": "技能【%s】冷却中,剩余 %.1f 秒。" % [cfg["name"], skill_cooldowns[skill_id]]
|
|
}
|
|
|
|
# 检查玩家法力 (SP)
|
|
var sp_cost: int = cfg["sp_cost"]
|
|
if player_sp < sp_cost:
|
|
return {
|
|
"ok": false,
|
|
"reason": "INSUFFICIENT_SP",
|
|
"cost": sp_cost,
|
|
"current_sp": player_sp,
|
|
"msg": "法力值不足,无法施展【%s】!" % cfg["name"]
|
|
}
|
|
|
|
# 检查马匹精力
|
|
if horse_stamina < 5.0:
|
|
return {
|
|
"ok": false,
|
|
"reason": "HORSE_EXHAUSTED",
|
|
"msg": "战马体力已透支,请先喂食胡萝卜或人参!"
|
|
}
|
|
|
|
# 施法成功:扣除精力、进入冷却
|
|
horse_stamina = maxf(0.0, horse_stamina - 5.0)
|
|
skill_cooldowns[skill_id] = cfg["cooldown"]
|
|
horse_stamina_changed.emit(horse_stamina, max_stamina)
|
|
horse_skill_used.emit(skill_id, cfg["name"], sp_cost)
|
|
|
|
var dealt_dmg := int(base_att * cfg["dmg_mult"])
|
|
|
|
return {
|
|
"ok": true,
|
|
"skill_id": skill_id,
|
|
"skill_name": cfg["name"],
|
|
"sp_cost": sp_cost,
|
|
"cooldown": cfg["cooldown"],
|
|
"damage": dealt_dmg,
|
|
"effect": cfg["effect"],
|
|
"range": cfg["range"],
|
|
"msg": "施展了战马技能【%s】!" % cfg["name"]
|
|
}
|
|
|
|
# 喂食恢复马匹精力 (胡萝卜 +40, 人参 +80)
|
|
func feed_horse(food_vnum: int) -> Dictionary:
|
|
if horse_tier == HORSE_TIER_NONE:
|
|
return {"ok": false, "reason": "NO_HORSE", "msg": "当前没有马匹!"}
|
|
|
|
var restore_amount := 0.0
|
|
var food_name := ""
|
|
if food_vnum == ITEM_FOOD_CARROT:
|
|
restore_amount = 40.0
|
|
food_name = "胡萝卜"
|
|
elif food_vnum == ITEM_FOOD_GINSENG:
|
|
restore_amount = 80.0
|
|
food_name = "人参"
|
|
else:
|
|
return {"ok": false, "reason": "WRONG_FOOD", "msg": "这不是战马适合的食物!"}
|
|
|
|
horse_stamina = minf(max_stamina, horse_stamina + restore_amount)
|
|
horse_stamina_changed.emit(horse_stamina, max_stamina)
|
|
|
|
return {
|
|
"ok": true,
|
|
"food_name": food_name,
|
|
"restored": restore_amount,
|
|
"current_stamina": horse_stamina,
|
|
"msg": "战马食用了【%s】,恢复了体力!" % food_name
|
|
}
|
|
|
|
# 冷却逐帧更新
|
|
func update(dt: float) -> void:
|
|
for sk in skill_cooldowns.keys():
|
|
if skill_cooldowns[sk] > 0.0:
|
|
skill_cooldowns[sk] = maxf(0.0, skill_cooldowns[sk] - dt)
|