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

239 lines
7.4 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.
# 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))