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

313 lines
11 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.
# 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)