- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
284 lines
9.8 KiB
GDScript
284 lines
9.8 KiB
GDScript
# horse_level_growth_system.gd —— Metin2 40250 官方马匹等级进化与养殖草料喂食系统 1:1
|
||
# 对照 40250 服务端 horse_rider.cpp, char_horse.cpp, constants.h
|
||
class_name HorseLevelGrowthSystem
|
||
extends RefCounted
|
||
|
||
signal horse_level_up(new_level: int, tier_name: String)
|
||
signal horse_fed(new_health: int, max_health: int)
|
||
signal horse_revived(new_health: int)
|
||
signal horse_mounted(horse_level: int, stats_applied: Dictionary)
|
||
signal horse_dismounted()
|
||
signal stamina_exhausted()
|
||
|
||
# 40250 官方马牌与召唤道具
|
||
const VNUM_HORSE_PICTURE := 50051 # 马牌 (初级小马召唤道具)
|
||
const VNUM_ARMOR_HORSE_BOOK := 50052 # 战马书 (战斗装甲马召唤道具)
|
||
const VNUM_MILITARY_HORSE_BOOK:= 50053 # 军马令 (军阶战马召唤道具)
|
||
|
||
# 40250 官方草料饲料
|
||
const VNUM_HAY := 50054 # 干草 (初级小马食物)
|
||
const VNUM_CARROT := 50055 # 胡萝卜 (战斗装甲马食物)
|
||
const VNUM_RED_GINSENG := 50056 # 红参 (军阶战马食物)
|
||
|
||
# 40250 官方复活猴草
|
||
const VNUM_HERB_EASY := 50057 # 初级猴草 (复活初级小马)
|
||
const VNUM_HERB_NORMAL := 50058 # 中级猴草 (复活战斗装甲马)
|
||
const VNUM_HERB_EXPERT := 50059 # 高级猴草 (复活军阶战马)
|
||
|
||
# 3 大战马阶层定义
|
||
const TIER_NONE := 0
|
||
const TIER_BEGINNER := 1 # 1~10 级 初级小马 (Race 20101, 人物需求 Lv 25)
|
||
const TIER_COMBAT := 2 # 11~20 级 战斗装甲马 (Race 20104, 人物需求 Lv 35)
|
||
const TIER_MILITARY := 3 # 21~30 级 军阶战马 (Race 20107, 人物需求 Lv 50)
|
||
|
||
# 40250 官方经典等级属性表 (选样代表级与通用成长模型)
|
||
const HORSE_STAT_TABLE: Dictionary = {
|
||
1: {"min_lv": 25, "race": 20101, "max_health": 3, "max_stamina": 4, "st": 26, "dx": 35, "ht": 18, "iq": 9, "atk": 54, "def": 32},
|
||
5: {"min_lv": 25, "race": 20101, "max_health": 8, "max_stamina": 6, "st": 30, "dx": 40, "ht": 20, "iq": 10, "atk": 58, "def": 34},
|
||
10: {"min_lv": 25, "race": 20101, "max_health": 15, "max_stamina": 10, "st": 35, "dx": 46, "ht": 23, "iq": 12, "atk": 63, "def": 37},
|
||
11: {"min_lv": 35, "race": 20104, "max_health": 18, "max_stamina": 30, "st": 40, "dx": 53, "ht": 27, "iq": 13, "atk": 69, "def": 41},
|
||
15: {"min_lv": 35, "race": 20104, "max_health": 24, "max_stamina": 55, "st": 44, "dx": 58, "ht": 29, "iq": 15, "atk": 73, "def": 43},
|
||
20: {"min_lv": 35, "race": 20104, "max_health": 32, "max_stamina": 100, "st": 48, "dx": 64, "ht": 32, "iq": 16, "atk": 78, "def": 46},
|
||
21: {"min_lv": 50, "race": 20107, "max_health": 35, "max_stamina": 120, "st": 53, "dx": 71, "ht": 36, "iq": 18, "atk": 84, "def": 50},
|
||
25: {"min_lv": 50, "race": 20107, "max_health": 40, "max_stamina": 140, "st": 60, "dx": 80, "ht": 40, "iq": 20, "atk": 91, "def": 54},
|
||
30: {"min_lv": 50, "race": 20107, "max_health": 50, "max_stamina": 200, "st": 67, "dx": 89, "ht": 45, "iq": 22, "atk": 99, "def": 59}
|
||
}
|
||
|
||
var horse_level: int = 0
|
||
var current_health: int = 0
|
||
var current_stamina: int = 0
|
||
var is_mounted: bool = false
|
||
var applied_stats: Dictionary = {}
|
||
|
||
# 获取当前战马阶层
|
||
func get_tier() -> int:
|
||
if horse_level <= 0:
|
||
return TIER_NONE
|
||
elif horse_level <= 10:
|
||
return TIER_BEGINNER
|
||
elif horse_level <= 20:
|
||
return TIER_COMBAT
|
||
else:
|
||
return TIER_MILITARY
|
||
|
||
# 获取阶层名称
|
||
func get_tier_name() -> String:
|
||
match get_tier():
|
||
TIER_BEGINNER:
|
||
return "初级小马"
|
||
TIER_COMBAT:
|
||
return "战斗装甲马"
|
||
TIER_MILITARY:
|
||
return "军阶战马"
|
||
_:
|
||
return "无战马"
|
||
|
||
# 获取对应等级的基础属性
|
||
func get_horse_stat(lv: int) -> Dictionary:
|
||
if HORSE_STAT_TABLE.has(lv):
|
||
return HORSE_STAT_TABLE[lv].duplicate(true)
|
||
# 近似插值计算
|
||
var base_lv := 1
|
||
for k in HORSE_STAT_TABLE.keys():
|
||
if int(k) <= lv and int(k) > base_lv:
|
||
base_lv = int(k)
|
||
return HORSE_STAT_TABLE[base_lv].duplicate(true)
|
||
|
||
# 初始化/领养战马
|
||
func initialize_horse(level: int = 1) -> void:
|
||
horse_level = clampi(level, 1, 30)
|
||
var stat = get_horse_stat(horse_level)
|
||
current_health = int(stat["max_health"])
|
||
current_stamina = int(stat["max_stamina"])
|
||
|
||
# 马匹等级升级
|
||
func upgrade_horse_level(char_level: int) -> Dictionary:
|
||
if horse_level >= 30:
|
||
return {"ok": false, "reason": "MAX_HORSE_LEVEL", "msg": "战马已达最高等级 30 级!"}
|
||
|
||
var target_lv := horse_level + 1
|
||
var target_stat := get_horse_stat(target_lv)
|
||
var req_char_lv := int(target_stat["min_lv"])
|
||
|
||
if char_level < req_char_lv:
|
||
return {
|
||
"ok": false,
|
||
"reason": "CHAR_LEVEL_TOO_LOW",
|
||
"msg": "角色等级不足!晋升至 %d 级战马需要角色达到 %d 级。" % [target_lv, req_char_lv]
|
||
}
|
||
|
||
horse_level = target_lv
|
||
var new_stat := get_horse_stat(horse_level)
|
||
current_health = int(new_stat["max_health"])
|
||
current_stamina = int(new_stat["max_stamina"])
|
||
|
||
horse_level_up.emit(horse_level, get_tier_name())
|
||
|
||
return {
|
||
"ok": true,
|
||
"horse_level": horse_level,
|
||
"tier": get_tier_name(),
|
||
"msg": "战马成功进阶至 %d 级【%s】!" % [horse_level, get_tier_name()]
|
||
}
|
||
|
||
# 喂食草料恢复健康值 (40250 horse_rider.cpp:FeedHorse)
|
||
func feed_horse(inventory: Array, slot_index: int) -> Dictionary:
|
||
if horse_level <= 0:
|
||
return {"ok": false, "reason": "NO_HORSE"}
|
||
|
||
if current_health <= 0:
|
||
return {"ok": false, "reason": "HORSE_IS_DEAD", "msg": "战马已重伤休克!需要喂食复活猴草唤醒。"}
|
||
|
||
var stat := get_horse_stat(horse_level)
|
||
var max_hp := int(stat["max_health"])
|
||
if current_health >= max_hp:
|
||
return {"ok": false, "reason": "HEALTH_ALREADY_FULL", "msg": "战马饱食度与健康值已满!"}
|
||
|
||
if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null:
|
||
return {"ok": false, "reason": "INVALID_SLOT"}
|
||
|
||
var food: Dictionary = inventory[slot_index]
|
||
var food_vnum := int(food.get("vnum", 0))
|
||
var tier := get_tier()
|
||
|
||
# 校验草料与阶层匹配 (40250 规则: 初级吃干草, 战斗吃胡萝卜, 军马吃红参)
|
||
var match_ok := false
|
||
if tier == TIER_BEGINNER and food_vnum == VNUM_HAY:
|
||
match_ok = true
|
||
elif tier == TIER_COMBAT and food_vnum == VNUM_CARROT:
|
||
match_ok = true
|
||
elif tier == TIER_MILITARY and food_vnum == VNUM_RED_GINSENG:
|
||
match_ok = true
|
||
|
||
if not match_ok:
|
||
return {"ok": false, "reason": "FOOD_TIER_MISMATCH", "msg": "该饲料与当前战马阶层不匹配!"}
|
||
|
||
# 消耗 1 份食物
|
||
var cnt := int(food.get("count", 1))
|
||
if cnt > 1:
|
||
food["count"] = cnt - 1
|
||
else:
|
||
inventory[slot_index] = null
|
||
|
||
current_health = mini(max_hp, current_health + 2)
|
||
horse_fed.emit(current_health, max_hp)
|
||
|
||
return {
|
||
"ok": true,
|
||
"current_health": current_health,
|
||
"max_health": max_hp,
|
||
"msg": "战马享用了饲料,健康值恢复至 %d/%d!" % [current_health, max_hp]
|
||
}
|
||
|
||
# 使用猴草唤醒复活战马
|
||
func revive_horse(inventory: Array, slot_index: int) -> Dictionary:
|
||
if current_health > 0:
|
||
return {"ok": false, "reason": "HORSE_NOT_DEAD", "msg": "战马依然健在,无需使用复活猴草!"}
|
||
|
||
if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null:
|
||
return {"ok": false, "reason": "INVALID_SLOT"}
|
||
|
||
var herb: Dictionary = inventory[slot_index]
|
||
var herb_vnum := int(herb.get("vnum", 0))
|
||
var tier := get_tier()
|
||
|
||
var herb_ok := false
|
||
if tier == TIER_BEGINNER and herb_vnum == VNUM_HERB_EASY:
|
||
herb_ok = true
|
||
elif tier == TIER_COMBAT and herb_vnum == VNUM_HERB_NORMAL:
|
||
herb_ok = true
|
||
elif tier == TIER_MILITARY and herb_vnum == VNUM_HERB_EXPERT:
|
||
herb_ok = true
|
||
|
||
if not herb_ok:
|
||
return {"ok": false, "reason": "HERB_TIER_MISMATCH", "msg": "复活猴草阶层与战马不匹配!"}
|
||
|
||
# 消耗 1 株猴草
|
||
var cnt := int(herb.get("count", 1))
|
||
if cnt > 1:
|
||
herb["count"] = cnt - 1
|
||
else:
|
||
inventory[slot_index] = null
|
||
|
||
var stat := get_horse_stat(horse_level)
|
||
current_health = maxi(1, int(stat["max_health"]) / 2)
|
||
current_stamina = maxi(1, int(stat["max_stamina"]) / 2)
|
||
|
||
horse_revived.emit(current_health)
|
||
|
||
return {
|
||
"ok": true,
|
||
"health": current_health,
|
||
"msg": "灵草生效!战马重焕生机复活苏醒。"
|
||
}
|
||
|
||
# 骑乘上马
|
||
func mount_horse(player_stats: Dictionary) -> Dictionary:
|
||
if horse_level <= 0:
|
||
return {"ok": false, "reason": "NO_HORSE", "msg": "尚未获得战马!"}
|
||
|
||
if current_health <= 0:
|
||
return {"ok": false, "reason": "HORSE_IS_DEAD", "msg": "战马已昏迷,需先喂食猴草复活!"}
|
||
|
||
if is_mounted:
|
||
return {"ok": false, "reason": "ALREADY_MOUNTED"}
|
||
|
||
var stat := get_horse_stat(horse_level)
|
||
# 注入战马加成
|
||
applied_stats = {
|
||
"attack_power": int(stat["atk"]),
|
||
"defense": int(stat["def"])
|
||
}
|
||
for k in applied_stats.keys():
|
||
player_stats[k] = int(player_stats.get(k, 0)) + applied_stats[k]
|
||
|
||
is_mounted = true
|
||
horse_mounted.emit(horse_level, applied_stats)
|
||
|
||
return {"ok": true, "msg": "成功乘骑战马!人马合一属性生效。"}
|
||
|
||
# 下马
|
||
func dismount_horse(player_stats: Dictionary) -> Dictionary:
|
||
if not is_mounted:
|
||
return {"ok": false, "reason": "NOT_MOUNTED"}
|
||
|
||
for k in applied_stats.keys():
|
||
player_stats[k] = int(player_stats.get(k, 0)) - applied_stats[k]
|
||
applied_stats.clear()
|
||
|
||
is_mounted = false
|
||
horse_dismounted.emit()
|
||
|
||
return {"ok": true, "msg": "已安全下马。"}
|
||
|
||
# 消耗耐力
|
||
func consume_stamina(amount: int, player_stats: Dictionary) -> void:
|
||
if not is_mounted:
|
||
return
|
||
current_stamina = maxi(0, current_stamina - amount)
|
||
if current_stamina <= 0:
|
||
stamina_exhausted.emit()
|
||
dismount_horse(player_stats) # 耐力耗尽自动下马
|
||
|
||
# 恢复耐力
|
||
func recover_stamina(amount: int) -> void:
|
||
var stat := get_horse_stat(horse_level)
|
||
var max_stamina := int(stat["max_stamina"])
|
||
current_stamina = mini(max_stamina, current_stamina + amount)
|
||
|
||
# 序列化
|
||
func serialize() -> Dictionary:
|
||
return {
|
||
"horse_level": horse_level,
|
||
"current_health": current_health,
|
||
"current_stamina": current_stamina,
|
||
"is_mounted": is_mounted
|
||
}
|
||
|
||
# 反序列化
|
||
func deserialize(data: Dictionary) -> void:
|
||
horse_level = int(data.get("horse_level", 0))
|
||
current_health = int(data.get("current_health", 0))
|
||
current_stamina = int(data.get("current_stamina", 0))
|
||
is_mounted = bool(data.get("is_mounted", false))
|