- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
139 lines
4.8 KiB
GDScript
139 lines
4.8 KiB
GDScript
# horse_system.gd —— 40250 战马与骑乘系统 1:1 规则引擎
|
||
# 严格对照:
|
||
# metin2/src/server/game/src/cmd_general.cpp:45-100 (do_user_horse_ride / back / feed)
|
||
# metin2/src/server/game/src/cmd_general.cpp:2426-2465 (do_ride)
|
||
# root/game.py:344, 461-490 (Ctrl+G, Ctrl+H, Ctrl+B, Ctrl+F)
|
||
# ClientVS22/UserInterface/InstanceBase.cpp (IsMountingHorse, 5.0m attack range)
|
||
extends RefCounted
|
||
|
||
const ITEM_HORSE_PICTURE := 50051 # 初级马画 (Lv 1 - 10)
|
||
const ITEM_HORSE_MEDAL := 50052 # 战斗马牌 (Lv 11 - 20)
|
||
const ITEM_HORSE_MILITARY := 50053 # 军马牌 (Lv 21 - 30)
|
||
|
||
const ITEM_FOOD_HAY := 50057 # 干草
|
||
const ITEM_FOOD_CARROT := 50054 # 胡萝卜
|
||
const ITEM_FOOD_GINSENG := 50058 # 人参
|
||
|
||
const HORSE_TIER_NONE := 0
|
||
const HORSE_TIER_BEGINNER := 1 # 普通马
|
||
const HORSE_TIER_COMBAT := 2 # 战斗马(可马上砍怪)
|
||
const HORSE_TIER_MILITARY := 3 # 军马(可使用马术技能)
|
||
|
||
# 召唤消耗 SP
|
||
const SUMMON_SP_COST := {
|
||
HORSE_TIER_BEGINNER: 100,
|
||
HORSE_TIER_COMBAT: 200,
|
||
HORSE_TIER_MILITARY: 300,
|
||
}
|
||
|
||
# 基础召唤成功率
|
||
const BASE_SUMMON_CHANCE := {
|
||
HORSE_TIER_BEGINNER: 10,
|
||
HORSE_TIER_COMBAT: 20,
|
||
HORSE_TIER_MILITARY: 30,
|
||
}
|
||
|
||
# 骑乘时战斗与拾取距离加成(从 300cm 扩至 500cm,1:1 ClientVS22)
|
||
const WALKING_ATTACK_RANGE := 3.0
|
||
const MOUNTED_ATTACK_RANGE := 5.0
|
||
|
||
# 判定马牌对应的级别与需求
|
||
static func get_horse_tier_from_item(vnum: int) -> int:
|
||
match vnum:
|
||
ITEM_HORSE_PICTURE: return HORSE_TIER_BEGINNER
|
||
ITEM_HORSE_MEDAL: return HORSE_TIER_COMBAT
|
||
ITEM_HORSE_MILITARY: return HORSE_TIER_MILITARY
|
||
_: return HORSE_TIER_NONE
|
||
|
||
# 所需饲料
|
||
static func get_required_food(tier: int) -> int:
|
||
match tier:
|
||
HORSE_TIER_BEGINNER: return ITEM_FOOD_HAY
|
||
HORSE_TIER_COMBAT: return ITEM_FOOD_CARROT
|
||
HORSE_TIER_MILITARY: return ITEM_FOOD_GINSENG
|
||
_: return 0
|
||
|
||
# 召唤几率计算(1:1 char_horse.cpp: HorseSummon)
|
||
# chance = base_chance + horse_summon_skill_level * 10, max 100
|
||
static func calculate_summon_chance(tier: int, summon_skill_level: int) -> int:
|
||
var base: int = BASE_SUMMON_CHANCE.get(tier, 10)
|
||
return mini(100, base + summon_skill_level * 10)
|
||
|
||
# 执行召唤判定
|
||
static func try_summon_horse(
|
||
tier: int,
|
||
current_sp: int,
|
||
summon_skill_level: int,
|
||
roll_val := -1
|
||
) -> Dictionary:
|
||
var sp_cost: int = SUMMON_SP_COST.get(tier, 100)
|
||
if current_sp < sp_cost:
|
||
return {"success": false, "err": "insufficient_sp", "sp_cost": sp_cost}
|
||
|
||
var chance := calculate_summon_chance(tier, summon_skill_level)
|
||
var dice := roll_val if roll_val > 0 else (randi() % 100 + 1)
|
||
var success := (dice <= chance)
|
||
|
||
return {
|
||
"success": success,
|
||
"sp_cost": sp_cost,
|
||
"tier": tier,
|
||
"err": "" if success else "summon_failed",
|
||
}
|
||
|
||
# 执行 /user_horse_ride (Ctrl+H) 状态机 (cmd_general.cpp:45)
|
||
static func handle_user_horse_ride(
|
||
is_riding: bool,
|
||
is_horse_summoned: bool,
|
||
is_other_mount: bool,
|
||
is_dead_or_stun: bool
|
||
) -> Dictionary:
|
||
if is_dead_or_stun:
|
||
return {"action": "none", "err": "dead_or_stun"}
|
||
|
||
if not is_riding:
|
||
if is_other_mount:
|
||
return {"action": "none", "err": "already_mounting_other", "msg": "已经在使用其他坐骑。"}
|
||
if not is_horse_summoned:
|
||
return {"action": "none", "err": "horse_not_summoned", "msg": "请先召唤马匹。"}
|
||
return {"action": "start_riding", "err": "", "is_riding": true}
|
||
else:
|
||
return {"action": "stop_riding", "err": "", "is_riding": false}
|
||
|
||
# 执行 /user_horse_back (Ctrl+B) 状态机 (cmd_general.cpp:75)
|
||
static func handle_user_horse_back(
|
||
is_riding: bool,
|
||
is_horse_summoned: bool
|
||
) -> Dictionary:
|
||
if is_riding:
|
||
return {"action": "none", "err": "must_dismount_first", "msg": "请先下马。"}
|
||
if not is_horse_summoned:
|
||
return {"action": "none", "err": "horse_not_summoned", "msg": "请先召唤马匹。"}
|
||
return {"action": "unsummon", "err": "", "is_horse_summoned": false, "msg": "已送回马匹。"}
|
||
|
||
# 执行 /ride (Ctrl+G) 状态机 (cmd_general.cpp:2426)
|
||
static func handle_ride(
|
||
is_riding: bool,
|
||
is_horse_summoned: bool,
|
||
has_ride_item: bool
|
||
) -> Dictionary:
|
||
if is_riding:
|
||
return {"action": "stop_riding", "err": "", "is_riding": false}
|
||
if is_horse_summoned:
|
||
return {"action": "start_riding", "err": "", "is_riding": true}
|
||
if has_ride_item:
|
||
return {"action": "mount_item", "err": "", "is_riding": true}
|
||
return {"action": "none", "err": "cannot_ride", "msg": "没有可骑乘的马匹或坐骑。"}
|
||
|
||
# 喂食逻辑 (Ctrl+F)
|
||
static func handle_user_horse_feed(
|
||
tier: int,
|
||
food_item_vnum: int
|
||
) -> Dictionary:
|
||
var req_food := get_required_food(tier)
|
||
if req_food == 0:
|
||
return {"success": false, "msg": "当前没有出战马匹。"}
|
||
if food_item_vnum != req_food:
|
||
return {"success": false, "msg": "这种食物不适合当前马匹。"}
|
||
return {"success": true, "consumed_vnum": req_food, "msg": "战马饱餐了一顿,恢复了活力!"}
|