- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
313 lines
11 KiB
GDScript
313 lines
11 KiB
GDScript
# boss_summon_scroll_system.gd —— Metin2 40250 官方首领降临与魔石/矿脉召唤卷轴系统 1:1
|
||
# 100% 对照 40250 服务端 special_item_group.txt, char_item.cpp (ITEM_GIFTBOX), char_manager.cpp
|
||
class_name BossSummonScrollSystem
|
||
extends RefCounted
|
||
|
||
signal boss_summoned(mob_vnum: int, mob_name: String, spawn_pos: Vector2, is_aggressive: bool)
|
||
signal metin_summoned(metin_vnum: int, metin_name: String, spawn_pos: Vector2, level: int)
|
||
signal ore_vein_summoned(vein_vnum: int, vein_name: String, spawn_pos: Vector2)
|
||
signal summon_failed(reason: String)
|
||
|
||
# ==========================================
|
||
# 40250 官方首领召唤卷轴 (Boss Summon Scrolls)
|
||
# 对应 special_item_group.txt Vnum 71036 ~ 71043
|
||
# ==========================================
|
||
const BOSS_SCROLLS: Dictionary = {
|
||
71036: {
|
||
"name": "兽人首领召唤卷轴",
|
||
"tr_name": "Kağıt-Ork Şefi",
|
||
"mob_vnum": 691,
|
||
"mob_name": "兽人首领 (Orc Chief)",
|
||
"level": 50,
|
||
"rank": 4, # BOSS
|
||
"is_aggressive": true
|
||
},
|
||
71037: {
|
||
"name": "密教首领召唤卷轴",
|
||
"tr_name": "Kağıt-Karanlık Lider",
|
||
"mob_vnum": 791,
|
||
"mob_name": "黑风教主/密教首领 (Dark Leader)",
|
||
"level": 54,
|
||
"rank": 4,
|
||
"is_aggressive": true
|
||
},
|
||
71038: {
|
||
"name": "蜘蛛女王召唤卷轴",
|
||
"tr_name": "Kağıt-Kraliçe Örümcek",
|
||
"mob_vnum": 2091,
|
||
"mob_name": "蜘蛛女王 (Spider Queen)",
|
||
"level": 60,
|
||
"rank": 4,
|
||
"is_aggressive": true
|
||
},
|
||
71039: {
|
||
"name": "沙漠巨龟召唤卷轴",
|
||
"tr_name": "Kağıt-Kaplumbağa",
|
||
"mob_vnum": 2191,
|
||
"mob_name": "沙漠巨龟 (Giant Desert Tortoise)",
|
||
"level": 67,
|
||
"rank": 4,
|
||
"is_aggressive": true
|
||
},
|
||
71040: {
|
||
"name": "烈焰魔王召唤卷轴",
|
||
"tr_name": "Kağıt-Alev Kral",
|
||
"mob_vnum": 2206,
|
||
"mob_name": "烈焰魔王 (Flame King)",
|
||
"level": 73,
|
||
"rank": 4,
|
||
"is_aggressive": true
|
||
},
|
||
71041: {
|
||
"name": "九尾狐召唤卷轴",
|
||
"tr_name": "Kağıt-Dokuz Kuyruk",
|
||
"mob_vnum": 1901,
|
||
"mob_name": "九尾狐仙 (Nine Tails)",
|
||
"level": 72,
|
||
"rank": 4,
|
||
"is_aggressive": true
|
||
},
|
||
71042: {
|
||
"name": "恶魔之王召唤卷轴",
|
||
"tr_name": "Kağıt-Şeytan Kral",
|
||
"mob_vnum": 1091,
|
||
"mob_name": "恶魔之王 (Demon King)",
|
||
"level": 75,
|
||
"rank": 4,
|
||
"is_aggressive": true
|
||
},
|
||
71043: {
|
||
"name": "虎灵妖兽召唤卷轴",
|
||
"tr_name": "Sarı Kaplan H. Kağıdı",
|
||
"mob_vnum": 1304,
|
||
"mob_name": "黄虎幽灵 (Tiger Ghost)",
|
||
"level": 75,
|
||
"rank": 4,
|
||
"is_aggressive": true
|
||
}
|
||
}
|
||
|
||
# ==========================================
|
||
# 40250 官方魔石召唤卷轴 (Metin Summon Scrolls)
|
||
# 对应 special_item_group.txt Vnum 71080 ~ 71082
|
||
# ==========================================
|
||
const METIN_SCROLLS: Dictionary = {
|
||
71080: {
|
||
"name": "初级魔石召唤卷",
|
||
"tr_name": "Metin Taşı Çağırma Kağıdı (K)",
|
||
"pool": [
|
||
{"vnum": 8001, "name": "悲哀之石 (Metin of Sorrow)", "level": 5},
|
||
{"vnum": 8002, "name": "战斗之石 (Metin of Combat)", "level": 10},
|
||
{"vnum": 8003, "name": "会战之石 (Metin of Battle)", "level": 15},
|
||
{"vnum": 8004, "name": "贪婪之石 (Metin of Greed)", "level": 20},
|
||
{"vnum": 8005, "name": "漆黑之石 (Metin of Black)", "level": 25}
|
||
]
|
||
},
|
||
71081: {
|
||
"name": "中级魔石召唤卷",
|
||
"tr_name": "Metin Taşı Çağırma Kağıdı (O)",
|
||
"pool": [
|
||
{"vnum": 8006, "name": "黑暗之石 (Metin of Darkness)", "level": 30},
|
||
{"vnum": 8007, "name": "嫉妒之石 (Metin of Jealousy)", "level": 35},
|
||
{"vnum": 8008, "name": "灵魂之石 (Metin of Soul)", "level": 40},
|
||
{"vnum": 8009, "name": "阴影之石 (Metin of Shadow)", "level": 45},
|
||
{"vnum": 8010, "name": "强韧之石 (Metin of Toughness)", "level": 50}
|
||
]
|
||
},
|
||
71082: {
|
||
"name": "高级魔石召唤卷",
|
||
"tr_name": "Metin Taşı Çağırma Kağıdı (Z)",
|
||
"pool": [
|
||
{"vnum": 8011, "name": "恶魔之石 (Metin of Devil)", "level": 55},
|
||
{"vnum": 8012, "name": "陨落之石 (Metin of Fall)", "level": 60},
|
||
{"vnum": 8013, "name": "死者之石 (Metin of Death)", "level": 65},
|
||
{"vnum": 8014, "name": "屠杀之石 (Metin of Murder)", "level": 70},
|
||
{"vnum": 8024, "name": "狂风之石 (Metin of Pung-Ma)", "level": 75},
|
||
{"vnum": 8025, "name": "魔岩之石 (Metin of Ma-An)", "level": 80},
|
||
{"vnum": 8026, "name": "斗击之石 (Metin of Tu-Gyuk)", "level": 85},
|
||
{"vnum": 8027, "name": "绝望之石 (Metin of Jeon-Un)", "level": 90}
|
||
]
|
||
}
|
||
}
|
||
|
||
# ==========================================
|
||
# 40250 官方矿脉召唤卷轴 (Ore Vein Scrolls)
|
||
# 对应 special_item_group.txt Vnum 71057 ~ 71067
|
||
# ==========================================
|
||
const ORE_VEIN_SCROLLS: Dictionary = {
|
||
71057: {"mob_vnum": 20049, "name": "钻石矿脉 (Diamond Vein)"},
|
||
71058: {"mob_vnum": 20050, "name": "琥珀矿脉 (Amber Vein)"},
|
||
71059: {"mob_vnum": 20051, "name": "木化石矿脉 (Fossil Wood Vein)"},
|
||
71060: {"mob_vnum": 20052, "name": "铜矿脉 (Copper Vein)"},
|
||
71061: {"mob_vnum": 20053, "name": "银矿脉 (Silver Vein)"},
|
||
71062: {"mob_vnum": 20054, "name": "金矿脉 (Gold Vein)"},
|
||
71063: {"mob_vnum": 20055, "name": "翡翠矿脉 (Jade Vein)"},
|
||
71064: {"mob_vnum": 20056, "name": "黑檀木矿脉 (Ebony Vein)"},
|
||
71065: {"mob_vnum": 20057, "name": "珍珠矿脉 (Pearl Vein)"},
|
||
71066: {"mob_vnum": 20058, "name": "白金矿脉 (White Gold Vein)"},
|
||
71067: {"mob_vnum": 20059, "name": "水晶矿脉 (Crystal Vein)"}
|
||
}
|
||
|
||
# 当前已召唤生成的实体记录列表
|
||
var _active_summoned_entities: Array[Dictionary] = []
|
||
|
||
# 判断是否是 40250 原版召唤卷轴
|
||
func is_summon_scroll(vnum: int) -> bool:
|
||
return BOSS_SCROLLS.has(vnum) or METIN_SCROLLS.has(vnum) or ORE_VEIN_SCROLLS.has(vnum)
|
||
|
||
# 获取召唤卷轴类型类别
|
||
func get_scroll_category(vnum: int) -> String:
|
||
if BOSS_SCROLLS.has(vnum):
|
||
return "boss"
|
||
elif METIN_SCROLLS.has(vnum):
|
||
return "metin"
|
||
elif ORE_VEIN_SCROLLS.has(vnum):
|
||
return "ore_vein"
|
||
return ""
|
||
|
||
# 使用召唤卷轴
|
||
func use_summon_scroll(
|
||
inventory: Array,
|
||
slot_index: int,
|
||
player_pos: Vector2,
|
||
is_in_safe_zone: bool,
|
||
forced_choice_idx: int = -1
|
||
) -> Dictionary:
|
||
if slot_index < 0 or slot_index >= inventory.size():
|
||
summon_failed.emit("INVALID_SLOT")
|
||
return {"ok": false, "reason": "INVALID_SLOT", "msg": "无效的背包物品格位!"}
|
||
|
||
var item = inventory[slot_index]
|
||
if item == null:
|
||
summon_failed.emit("EMPTY_SLOT")
|
||
return {"ok": false, "reason": "EMPTY_SLOT", "msg": "指定格位没有任何道具!"}
|
||
|
||
var vnum = int(item.get("vnum", 0))
|
||
var category = get_scroll_category(vnum)
|
||
if category == "":
|
||
summon_failed.emit("NOT_A_SUMMON_SCROLL")
|
||
return {"ok": false, "reason": "NOT_A_SUMMON_SCROLL", "msg": "此物品并非怪物或魔石召唤卷轴!"}
|
||
|
||
# 40250 原版规则:安全区内禁止召唤具有攻击性的首领与魔石
|
||
if is_in_safe_zone and category != "ore_vein":
|
||
summon_failed.emit("CANNOT_SPAWN_IN_SAFEZONE")
|
||
return {"ok": false, "reason": "CANNOT_SPAWN_IN_SAFEZONE", "msg": "在安全区内无法召唤怪物!请移至野外危险区域。"}
|
||
|
||
# 扣除卷轴消耗 (40250 item->SetCount(item->GetCount()-1))
|
||
var current_count = int(item.get("count", 1))
|
||
if current_count > 1:
|
||
item["count"] = current_count - 1
|
||
else:
|
||
inventory[slot_index] = null
|
||
|
||
# 生成随机偏移位置 (类似 40250 GetX() + number(-500, 500))
|
||
var offset_x = randf_range(-120.0, 120.0)
|
||
var offset_y = randf_range(-120.0, 120.0)
|
||
# 确保不会生成在绝对重合点
|
||
if abs(offset_x) < 30.0:
|
||
offset_x = 50.0 if offset_x >= 0.0 else -50.0
|
||
if abs(offset_y) < 30.0:
|
||
offset_y = 50.0 if offset_y >= 0.0 else -50.0
|
||
|
||
var spawn_pos = Vector2(player_pos.x + offset_x, player_pos.y + offset_y)
|
||
var summoned_entity: Dictionary = {}
|
||
|
||
match category:
|
||
"boss":
|
||
var boss_info = BOSS_SCROLLS[vnum]
|
||
summoned_entity = {
|
||
"vnum": int(boss_info["mob_vnum"]),
|
||
"name": str(boss_info["mob_name"]),
|
||
"level": int(boss_info["level"]),
|
||
"rank": int(boss_info["rank"]),
|
||
"pos": spawn_pos,
|
||
"is_aggressive": true,
|
||
"category": "boss",
|
||
"summoner_pos": player_pos
|
||
}
|
||
_active_summoned_entities.append(summoned_entity)
|
||
boss_summoned.emit(summoned_entity["vnum"], summoned_entity["name"], spawn_pos, true)
|
||
|
||
"metin":
|
||
var metin_scroll = METIN_SCROLLS[vnum]
|
||
var pool: Array = metin_scroll["pool"]
|
||
var picked_idx = 0
|
||
if forced_choice_idx >= 0 and forced_choice_idx < pool.size():
|
||
picked_idx = forced_choice_idx
|
||
else:
|
||
picked_idx = randi() % pool.size()
|
||
|
||
var picked_metin = pool[picked_idx]
|
||
summoned_entity = {
|
||
"vnum": int(picked_metin["vnum"]),
|
||
"name": str(picked_metin["name"]),
|
||
"level": int(picked_metin["level"]),
|
||
"rank": 5, # STONE
|
||
"pos": spawn_pos,
|
||
"is_aggressive": false,
|
||
"category": "metin",
|
||
"summoner_pos": player_pos
|
||
}
|
||
_active_summoned_entities.append(summoned_entity)
|
||
metin_summoned.emit(summoned_entity["vnum"], summoned_entity["name"], spawn_pos, summoned_entity["level"])
|
||
|
||
"ore_vein":
|
||
var vein_info = ORE_VEIN_SCROLLS[vnum]
|
||
summoned_entity = {
|
||
"vnum": int(vein_info["mob_vnum"]),
|
||
"name": str(vein_info["name"]),
|
||
"pos": spawn_pos,
|
||
"is_aggressive": false,
|
||
"category": "ore_vein",
|
||
"summoner_pos": player_pos
|
||
}
|
||
_active_summoned_entities.append(summoned_entity)
|
||
ore_vein_summoned.emit(summoned_entity["vnum"], summoned_entity["name"], spawn_pos)
|
||
|
||
return {
|
||
"ok": true,
|
||
"category": category,
|
||
"entity": summoned_entity,
|
||
"msg": "卷轴散发出一道耀眼的光芒,强大的封印能量破空而出!"
|
||
}
|
||
|
||
# 获取当前已召唤的所有活跃实体
|
||
func get_active_summoned_entities() -> Array[Dictionary]:
|
||
return _active_summoned_entities
|
||
|
||
# 清空/移除已击败或离开的实体
|
||
func remove_summoned_entity(index: int) -> bool:
|
||
if index >= 0 and index < _active_summoned_entities.size():
|
||
_active_summoned_entities.remove_at(index)
|
||
return true
|
||
return false
|
||
|
||
func clear_summoned_entities() -> void:
|
||
_active_summoned_entities.clear()
|
||
|
||
# 存档与读档支持
|
||
func serialize() -> Dictionary:
|
||
var list_copy: Array = []
|
||
for e in _active_summoned_entities:
|
||
var pos_dict = {"x": e["pos"].x, "y": e["pos"].y}
|
||
var copy_e = e.duplicate()
|
||
copy_e["pos"] = pos_dict
|
||
if copy_e.has("summoner_pos"):
|
||
copy_e["summoner_pos"] = {"x": e["summoner_pos"].x, "y": e["summoner_pos"].y}
|
||
list_copy.append(copy_e)
|
||
return {
|
||
"active_entities": list_copy
|
||
}
|
||
|
||
func deserialize(data: Dictionary) -> void:
|
||
_active_summoned_entities.clear()
|
||
var raw_list = data.get("active_entities", [])
|
||
for item in raw_list:
|
||
var pos_data = item.get("pos", {"x": 0.0, "y": 0.0})
|
||
var ent = item.duplicate()
|
||
ent["pos"] = Vector2(pos_data.get("x", 0.0), pos_data.get("y", 0.0))
|
||
if ent.has("summoner_pos"):
|
||
var sp_data = ent.get("summoner_pos", {"x": 0.0, "y": 0.0})
|
||
ent["summoner_pos"] = Vector2(sp_data.get("x", 0.0), sp_data.get("y", 0.0))
|
||
_active_summoned_entities.append(ent)
|