Files
mtgodot-poc/project/horse_combat_system.gd
T
shenandshen 66d217b313 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 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

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)