Files
mtgodot-poc/project/horse_level_growth_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

284 lines
9.8 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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))