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

193 lines
6.7 KiB
GDScript

# compass_treasure_hunt_system.gd —— Metin2 40250 官方魔石地脉探测罗盘与寻宝系统 1:1
# 对照 40250 服务端 questlua_item.cpp, locale_en/itemdesc.txt:27989
class_name CompassTreasureHuntSystem
extends RefCounted
signal compass_scanned(target_name: String, distance: float, angle_deg: float, proximity: String, charges_left: int)
signal treasure_excavated(chest_vnum: int, chest_name: String)
signal treasure_opened(chest_name: String, rewards: Array)
# 40250 官方核心道具 Vnum
const VNUM_COMPASS_OF_METIN := 27989 # 魔石探测罗盘 (Compass for Metin Stones, 6次充能)
const VNUM_GOLD_CHEST := 50005 # 黄金宝箱
const VNUM_SILVER_CHEST := 50006 # 白银宝箱
const VNUM_GOLD_KEY := 50007 # 黄金钥匙
const VNUM_SILVER_KEY := 50008 # 白银钥匙
const MAX_CHARGES := 6
# 探测距离等级
const PROXIMITY_FAR := "FAR" # > 100m ("微弱微光 - 遥远")
const PROXIMITY_MEDIUM := "MEDIUM" # 50m ~ 100m ("泛起银光 - 渐近")
const PROXIMITY_NEAR := "NEAR" # 20m ~ 50m ("强烈明光 - 附近")
const PROXIMITY_VERY_CLOSE := "VERY_CLOSE" # < 20m ("璀璨耀光 - 极近")
# 激活魔石探测罗盘扫描最近的魔石或地脉宝藏
func scan_nearest_metin(player_pos: Vector2, target_list: Array, inventory: Array, slot_index: int) -> Dictionary:
if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null:
return {"ok": false, "reason": "INVALID_SLOT"}
var compass: Dictionary = inventory[slot_index]
if int(compass.get("vnum", 0)) != VNUM_COMPASS_OF_METIN:
return {"ok": false, "reason": "NOT_A_COMPASS", "msg": "该物品不是魔石探测罗盘!"}
var charges: int = int(compass.get("charges", MAX_CHARGES))
if charges <= 0:
inventory[slot_index] = null
return {"ok": false, "reason": "COMPASS_EXHAUSTED", "msg": "罗盘灵力已耗尽,化作飞灰!"}
if target_list.is_empty():
return {"ok": false, "reason": "NO_TARGETS_FOUND", "msg": "当前地图未探测到任何魔石波动!"}
# 寻找最近的目标
var nearest_target: Dictionary = {}
var min_dist := INF
for t in target_list:
var pos: Vector2 = t.get("pos", Vector2.ZERO)
var d := player_pos.distance_to(pos)
if d < min_dist:
min_dist = d
nearest_target = t
# 扣除 1 次充能计数 (40250 官方 6 次限制)
charges -= 1
compass["charges"] = charges
if charges <= 0:
inventory[slot_index] = null # 充能用完销毁
# 计算方位角与距离等级
var target_pos: Vector2 = nearest_target.get("pos", Vector2.ZERO)
var dir_vec: Vector2 = (target_pos - player_pos).normalized()
var angle := rad_to_deg(dir_vec.angle())
var proximity := PROXIMITY_FAR
var desc := "微弱微光(遥远)"
if min_dist < 20.0:
proximity = PROXIMITY_VERY_CLOSE
desc = "璀璨耀光(极近)"
elif min_dist < 50.0:
proximity = PROXIMITY_NEAR
desc = "强烈明光(附近)"
elif min_dist < 100.0:
proximity = PROXIMITY_MEDIUM
desc = "泛起银光(渐近)"
var target_name: String = str(nearest_target.get("name", "魔石"))
compass_scanned.emit(target_name, min_dist, angle, proximity, charges)
return {
"ok": true,
"target_name": target_name,
"target_pos": nearest_target.get("pos", Vector2.ZERO),
"distance": min_dist,
"angle_deg": angle,
"proximity": proximity,
"proximity_desc": desc,
"charges_left": charges,
"msg": "罗盘指针震颤!锁定【%s】,距离 %.1f 米,光芒等级:%s,剩余使用次数:%d。" % [target_name, min_dist, desc, charges]
}
# 在地脉极近坐标处挖掘秘宝
func excavate_treasure(player_pos: Vector2, treasure_nodes: Array, inventory: Array) -> Dictionary:
var target_idx := -1
for i in range(treasure_nodes.size()):
var node_pos: Vector2 = treasure_nodes[i].get("pos", Vector2.ZERO)
if player_pos.distance_to(node_pos) <= 5.0: # 5米范围内可挖掘
target_idx = i
break
if target_idx == -1:
return {"ok": false, "reason": "NO_TREASURE_NEARBY", "msg": "周围 5 米内未探测到地脉埋藏的宝箱!"}
var node: Dictionary = treasure_nodes[target_idx]
var is_gold: bool = bool(node.get("is_gold", false))
var chest_vnum = VNUM_GOLD_CHEST if is_gold else VNUM_SILVER_CHEST
var chest_name = "黄金宝箱" if is_gold else "白银宝箱"
# 寻找背包空格放入宝箱
var free_slot := -1
for i in range(inventory.size()):
if inventory[i] == null:
free_slot = i
break
if free_slot == -1:
return {"ok": false, "reason": "INVENTORY_FULL", "msg": "背包已满,无法容纳挖出的宝箱!"}
inventory[free_slot] = {
"vnum": chest_vnum,
"name": chest_name,
"count": 1
}
treasure_nodes.remove_at(target_idx)
treasure_excavated.emit(chest_vnum, chest_name)
return {
"ok": true,
"chest_vnum": chest_vnum,
"chest_name": chest_name,
"slot": free_slot,
"msg": "破土而出!在地脉深处成功发掘出【%s】!" % chest_name
}
# 使用对应的黄金钥匙或白银钥匙开启宝箱
func open_treasure_chest(chest_slot: int, key_slot: int, inventory: Array) -> Dictionary:
if chest_slot < 0 or chest_slot >= inventory.size() or inventory[chest_slot] == null:
return {"ok": false, "reason": "INVALID_CHEST_SLOT"}
if key_slot < 0 or key_slot >= inventory.size() or inventory[key_slot] == null:
return {"ok": false, "reason": "INVALID_KEY_SLOT"}
var chest: Dictionary = inventory[chest_slot]
var key: Dictionary = inventory[key_slot]
var chest_vnum := int(chest.get("vnum", 0))
var key_vnum := int(key.get("vnum", 0))
var match_ok := false
if chest_vnum == VNUM_GOLD_CHEST and key_vnum == VNUM_GOLD_KEY:
match_ok = true
elif chest_vnum == VNUM_SILVER_CHEST and key_vnum == VNUM_SILVER_KEY:
match_ok = true
if not match_ok:
return {"ok": false, "reason": "KEY_MISMATCH", "msg": "钥匙与宝箱类型不匹配!黄金宝箱需用黄金钥匙开启。"}
# 消耗钥匙与宝箱
inventory[chest_slot] = null
var key_cnt := int(key.get("count", 1))
if key_cnt > 1:
key["count"] = key_cnt - 1
else:
inventory[key_slot] = null
# 生成高额宝物奖励
var rewards: Array = []
if chest_vnum == VNUM_GOLD_CHEST:
rewards = [
{"vnum": 25040, "name": "祝福卷轴", "count": 3},
{"vnum": 51501, "name": "龙石原石", "count": 2},
{"vnum": 28430, "name": "+4 灵石", "count": 1}
]
else:
rewards = [
{"vnum": 25040, "name": "祝福卷轴", "count": 1},
{"vnum": 50300, "name": "技能书", "count": 1}
]
# 放入背包
for r in rewards:
for i in range(inventory.size()):
if inventory[i] == null:
inventory[i] = r.duplicate(true)
break
treasure_opened.emit(chest.get("name", "宝箱"), rewards)
return {
"ok": true,
"chest_name": chest.get("name", "宝箱"),
"rewards": rewards,
"msg": "清脆的开锁声响起!开启宝箱,获得珍稀秘宝!"
}