- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
239 lines
7.4 KiB
GDScript
239 lines
7.4 KiB
GDScript
# spider_dungeon_key_system.gd —— Metin2 40250 经典深渊蜘蛛洞穴通行证与蛛后巢穴密匙系统 1:1
|
||
# 对照 40250 服务端 spider_dungeon_3floor_boss.quest, char_item.cpp, dungeon.cpp
|
||
class_name SpiderDungeonKeySystem
|
||
extends RefCounted
|
||
|
||
signal ticket_consumed_entry_granted(floor: int, map_name: String)
|
||
signal arachnid_key_crafted(key_count: int)
|
||
signal baroness_lair_opened(time_limit: float)
|
||
signal baroness_slain(drop_items: Array)
|
||
signal lair_closed(success: bool)
|
||
|
||
# 40250 官方核心道具 Vnum
|
||
const VNUM_PASSAGE_TICKET := 71095 # 蛛洞通行证 (Passage Ticket)
|
||
const VNUM_ARACHNID_KEY := 30324 # 蛛网密匙 (Arachnid Key)
|
||
|
||
# 40250 蛛网密匙合成原材料
|
||
const VNUM_SPIDER_POISON_SACK := 30025 # 蜘蛛毒囊
|
||
const VNUM_SPIDER_EGG_SACK := 30056 # 蜘蛛卵囊
|
||
const VNUM_SPIDER_EYES := 30057 # 蜘蛛眼睛
|
||
const VNUM_SPIDER_LEGS := 30058 # 蜘蛛腿
|
||
const CRAFT_COST_GOLD := 50000 # 50,000 金币
|
||
|
||
# NPC Vnum
|
||
const NPC_SPIDER_GUARD := 20089 # 蛛洞入口守卫
|
||
const NPC_SCHOLAR_URIEL:= 20011 # 学者乌列尔 (合成密匙)
|
||
const NPC_LAIR_TOMBSTONE:= 30130 # 蛛后巢穴封印石碑
|
||
|
||
# Boss Vnum
|
||
const MOB_SPIDER_BARONESS := 2092 # 蜘蛛女皇/蛛后
|
||
const VNUM_CHEST_BARONESS := 50181 # 蛛后宝箱
|
||
|
||
# 巢穴限时 (20 分钟 / 1200 秒)
|
||
const LAIR_DURATION := 1200.0
|
||
|
||
var is_lair_active: bool = false
|
||
var lair_time_remaining: float = 0.0
|
||
var baroness_alive: bool = false
|
||
|
||
# 使用通行证进入蜘蛛洞穴 2层或3层
|
||
func enter_spider_dungeon(floor_target: int, player_level: int, inventory: Array) -> Dictionary:
|
||
if floor_target != 2 and floor_target != 3:
|
||
return {"ok": false, "reason": "INVALID_FLOOR", "msg": "只能前往蛛洞二层(SD2)或三层(SD3)!"}
|
||
|
||
var min_lv = 40 if floor_target == 2 else 50
|
||
if player_level < min_lv:
|
||
return {
|
||
"ok": false,
|
||
"reason": "LEVEL_TOO_LOW",
|
||
"msg": "实力不足!进入蛛洞 %d 层需要等级达到 %d 级。" % [floor_target, min_lv]
|
||
}
|
||
|
||
# 寻找并消耗 1 张通行证 (71095)
|
||
var ticket_slot := -1
|
||
for i in range(inventory.size()):
|
||
if inventory[i] != null and int(inventory[i].get("vnum", 0)) == VNUM_PASSAGE_TICKET:
|
||
ticket_slot = i
|
||
break
|
||
|
||
if ticket_slot == -1:
|
||
return {"ok": false, "reason": "NO_PASSAGE_TICKET", "msg": "守卫拦住了去路!需要出示【蛛洞通行证】(71095)。"}
|
||
|
||
var t_item: Dictionary = inventory[ticket_slot]
|
||
var cnt := int(t_item.get("count", 1))
|
||
if cnt > 1:
|
||
t_item["count"] = cnt - 1
|
||
else:
|
||
inventory[ticket_slot] = null
|
||
|
||
var target_map = "Spider_Dungeon_2" if floor_target == 2 else "Spider_Dungeon_3"
|
||
ticket_consumed_entry_granted.emit(floor_target, target_map)
|
||
|
||
return {
|
||
"ok": true,
|
||
"floor": floor_target,
|
||
"target_map": target_map,
|
||
"msg": "检票通过!守卫为您开启了通往【蛛洞 %d 层】的隐秘石门。" % floor_target
|
||
}
|
||
|
||
# 在学者乌列尔处精炼合成蛛网密匙 (40250 spider_dungeon_3floor_boss.quest)
|
||
func craft_arachnid_key(inventory: Array, player_gold: Dictionary) -> Dictionary:
|
||
var current_gold := int(player_gold.get("gold", 0))
|
||
if current_gold < CRAFT_COST_GOLD:
|
||
return {"ok": false, "reason": "NOT_ENOUGH_GOLD", "msg": "精炼费用不足!需要 %d 金币。" % CRAFT_COST_GOLD}
|
||
|
||
# 检查 4 大蜘蛛残肢素材
|
||
var req_vnums = [VNUM_SPIDER_POISON_SACK, VNUM_SPIDER_EGG_SACK, VNUM_SPIDER_EYES, VNUM_SPIDER_LEGS]
|
||
var slots_to_consume: Array = []
|
||
|
||
for req_vnum in req_vnums:
|
||
var found_slot := -1
|
||
for i in range(inventory.size()):
|
||
if inventory[i] != null and int(inventory[i].get("vnum", 0)) == req_vnum:
|
||
found_slot = i
|
||
break
|
||
if found_slot == -1:
|
||
return {"ok": false, "reason": "MISSING_MATERIALS", "msg": "合成材料不足!缺少蜘蛛残肢核心原料。"}
|
||
slots_to_consume.append(found_slot)
|
||
|
||
# 寻找放置生成密匙的空位
|
||
var free_slot := -1
|
||
for i in range(inventory.size()):
|
||
if inventory[i] == null:
|
||
free_slot = i
|
||
break
|
||
|
||
# 如果没有全新空位,但材料消耗后会腾出空位也是可行的
|
||
if free_slot == -1:
|
||
for s in slots_to_consume:
|
||
if int(inventory[s].get("count", 1)) == 1:
|
||
free_slot = s # 可以复用该格
|
||
break
|
||
|
||
if free_slot == -1:
|
||
return {"ok": false, "reason": "INVENTORY_FULL", "msg": "背包已满,无法容纳新炼制的密匙!"}
|
||
|
||
# 消耗材料与金币
|
||
player_gold["gold"] = current_gold - CRAFT_COST_GOLD
|
||
for s in slots_to_consume:
|
||
var it: Dictionary = inventory[s]
|
||
var c := int(it.get("count", 1))
|
||
if c > 1:
|
||
it["count"] = c - 1
|
||
else:
|
||
inventory[s] = null
|
||
|
||
# 产出蛛网密匙 (30324)
|
||
inventory[free_slot] = {
|
||
"vnum": VNUM_ARACHNID_KEY,
|
||
"name": "蛛网密匙",
|
||
"count": 1
|
||
}
|
||
|
||
arachnid_key_crafted.emit(1)
|
||
|
||
return {
|
||
"ok": true,
|
||
"key_vnum": VNUM_ARACHNID_KEY,
|
||
"slot": free_slot,
|
||
"msg": "学者乌列尔成功为您炼制了【蛛网密匙】(30324)!"
|
||
}
|
||
|
||
# 封印石碑检票开启蛛后巢穴 (40250 NPC 30130)
|
||
func open_baroness_lair(player_level: int, inventory: Array) -> Dictionary:
|
||
if is_lair_active:
|
||
return {"ok": false, "reason": "LAIR_ALREADY_ACTIVE", "msg": "蛛后巢穴已在挑战中!"}
|
||
|
||
if player_level < 50:
|
||
return {"ok": false, "reason": "LEVEL_TOO_LOW", "msg": "挑战蛛后需要角色等级达到 50 级!"}
|
||
|
||
# 寻找并消耗蛛网密匙 (30324)
|
||
var key_slot := -1
|
||
for i in range(inventory.size()):
|
||
if inventory[i] != null and int(inventory[i].get("vnum", 0)) == VNUM_ARACHNID_KEY:
|
||
key_slot = i
|
||
break
|
||
|
||
if key_slot == -1:
|
||
return {"ok": false, "reason": "NO_ARACHNID_KEY", "msg": "封印石碑毫无反应!需要镶嵌【蛛网密匙】(30324)。"}
|
||
|
||
# 消耗钥匙
|
||
var k_item: Dictionary = inventory[key_slot]
|
||
var cnt := int(k_item.get("count", 1))
|
||
if cnt > 1:
|
||
k_item["count"] = cnt - 1
|
||
else:
|
||
inventory[key_slot] = null
|
||
|
||
is_lair_active = true
|
||
lair_time_remaining = LAIR_DURATION
|
||
baroness_alive = true
|
||
|
||
baroness_lair_opened.emit(LAIR_DURATION)
|
||
|
||
return {
|
||
"ok": true,
|
||
"duration": LAIR_DURATION,
|
||
"boss_vnum": MOB_SPIDER_BARONESS,
|
||
"msg": "石碑封印破除!蛛网密室大门洞开,限时 20 分钟讨伐蜘蛛女皇!"
|
||
}
|
||
|
||
# 击败蜘蛛女皇
|
||
func slay_baroness(inventory: Array) -> Dictionary:
|
||
if not is_lair_active or not baroness_alive:
|
||
return {"ok": false, "reason": "BARONESS_NOT_ALIVE"}
|
||
|
||
baroness_alive = false
|
||
|
||
var drops: Array = [
|
||
{"vnum": VNUM_CHEST_BARONESS, "name": "蛛后宝箱", "count": 1},
|
||
{"vnum": 25040, "name": "祝福卷轴", "count": 2}
|
||
]
|
||
|
||
# 放入背包
|
||
for drop in drops:
|
||
for i in range(inventory.size()):
|
||
if inventory[i] == null:
|
||
inventory[i] = drop.duplicate(true)
|
||
break
|
||
|
||
baroness_slain.emit(drops)
|
||
|
||
# 胜利结束
|
||
close_lair(true)
|
||
|
||
return {
|
||
"ok": true,
|
||
"drops": drops,
|
||
"msg": "诛灭深渊蛛后!成功夺得【蛛后宝箱】!"
|
||
}
|
||
|
||
# 关闭巢穴副本
|
||
func close_lair(success: bool) -> void:
|
||
is_lair_active = false
|
||
lair_time_remaining = 0.0
|
||
baroness_alive = false
|
||
lair_closed.emit(success)
|
||
|
||
# 时钟心跳推进
|
||
func update(delta: float) -> void:
|
||
if not is_lair_active:
|
||
return
|
||
lair_time_remaining -= delta
|
||
if lair_time_remaining <= 0.0:
|
||
close_lair(false) # 超时失败关闭
|
||
|
||
# 序列化
|
||
func serialize() -> Dictionary:
|
||
return {
|
||
"is_lair_active": is_lair_active,
|
||
"lair_time_remaining": lair_time_remaining,
|
||
"baroness_alive": baroness_alive
|
||
}
|
||
|
||
# 反序列化
|
||
func deserialize(data: Dictionary) -> void:
|
||
is_lair_active = bool(data.get("is_lair_active", false))
|
||
lair_time_remaining = float(data.get("lair_time_remaining", 0.0))
|
||
baroness_alive = bool(data.get("baroness_alive", false))
|