- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
146 lines
4.6 KiB
GDScript
146 lines
4.6 KiB
GDScript
# portable_service_system.gd —— Metin2 40250 随身便民道具与即时服务系统 1:1
|
|
# 对照 40250 服务端 char_item.cpp, cmd_general.cpp, Items 70005, 70006, 70007
|
|
class_name PortableServiceSystem
|
|
extends RefCounted
|
|
|
|
signal service_opened(service_type: String)
|
|
signal portable_item_consumed(vnum: int, remaining: int)
|
|
|
|
const ITEM_PORTABLE_SAFEBOX := 70005 # 随身仓库卷轴
|
|
const ITEM_PORTABLE_BLACKSMITH := 70006 # 随身铁匠手稿
|
|
const ITEM_PORTABLE_GENERAL_STORE := 70007 # 随身杂货铺契约
|
|
|
|
# 随身杂货铺基础货架商品列表 (大红、大蓝、加速药水、回城卷轴)
|
|
const PORTABLE_STORE_GOODS: Array = [
|
|
{"vnum": 27003, "name": "红药水(大)", "price": 3200, "count": 200},
|
|
{"vnum": 27006, "name": "蓝药水(大)", "price": 4800, "count": 200},
|
|
{"vnum": 27102, "name": "绿药水(大)", "price": 10000, "count": 10},
|
|
{"vnum": 22000, "name": "回城卷轴", "price": 2000, "count": 1}
|
|
]
|
|
|
|
var active_service: String = "" # "safebox", "blacksmith", "store", or ""
|
|
|
|
# 使用随身仓库 (70005)
|
|
func use_portable_safebox(slot_idx: int, inventory: Array) -> Dictionary:
|
|
var consume_res := _consume_portable_item(slot_idx, inventory, ITEM_PORTABLE_SAFEBOX)
|
|
if not consume_res["ok"]:
|
|
return consume_res
|
|
|
|
active_service = "safebox"
|
|
service_opened.emit("safebox")
|
|
|
|
return {
|
|
"ok": true,
|
|
"service": "safebox",
|
|
"remaining_scrolls": consume_res["remaining"],
|
|
"msg": "使用了【随身仓库卷轴】,已建立与城市货郎的远程仓库连接!"
|
|
}
|
|
|
|
# 使用随身铁匠 (70006)
|
|
func use_portable_blacksmith(slot_idx: int, inventory: Array) -> Dictionary:
|
|
var consume_res := _consume_portable_item(slot_idx, inventory, ITEM_PORTABLE_BLACKSMITH)
|
|
if not consume_res["ok"]:
|
|
return consume_res
|
|
|
|
active_service = "blacksmith"
|
|
service_opened.emit("blacksmith")
|
|
|
|
return {
|
|
"ok": true,
|
|
"service": "blacksmith",
|
|
"remaining_scrolls": consume_res["remaining"],
|
|
"msg": "展开了【随身铁匠手稿】,便携精炼锻造炉已点燃!随时可强化装备!"
|
|
}
|
|
|
|
# 使用随身杂货铺 (70007)
|
|
func use_portable_store(slot_idx: int, inventory: Array) -> Dictionary:
|
|
var consume_res := _consume_portable_item(slot_idx, inventory, ITEM_PORTABLE_GENERAL_STORE)
|
|
if not consume_res["ok"]:
|
|
return consume_res
|
|
|
|
active_service = "store"
|
|
service_opened.emit("store")
|
|
|
|
return {
|
|
"ok": true,
|
|
"service": "store",
|
|
"goods": PORTABLE_STORE_GOODS.duplicate(true),
|
|
"remaining_scrolls": consume_res["remaining"],
|
|
"msg": "出示了【随身杂货铺契约】,随身货郎已赶来!随时可补给药水或出售垃圾!"
|
|
}
|
|
|
|
# 从随身杂货铺购买商品
|
|
func buy_goods_from_portable_store(
|
|
goods_idx: int,
|
|
player_data: Dictionary,
|
|
inventory: Array
|
|
) -> Dictionary:
|
|
if active_service != "store":
|
|
return {"ok": false, "reason": "STORE_NOT_OPEN", "msg": "随身杂货铺尚未开启!"}
|
|
|
|
if goods_idx < 0 or goods_idx >= PORTABLE_STORE_GOODS.size():
|
|
return {"ok": false, "reason": "INVALID_GOODS", "msg": "商品不存在!"}
|
|
|
|
var goods: Dictionary = PORTABLE_STORE_GOODS[goods_idx]
|
|
var price: int = int(goods["price"])
|
|
var current_gold: int = int(player_data.get("gold", 0))
|
|
|
|
if current_gold < price:
|
|
return {
|
|
"ok": false,
|
|
"reason": "INSUFFICIENT_YANG",
|
|
"price": price,
|
|
"current_gold": current_gold,
|
|
"msg": "金币不足,无法购买!"
|
|
}
|
|
|
|
# 寻找背包空位
|
|
var empty_slot := -1
|
|
for i in range(inventory.size()):
|
|
if inventory[i] == null:
|
|
empty_slot = i
|
|
break
|
|
|
|
if empty_slot == -1:
|
|
return {"ok": false, "reason": "INVENTORY_FULL", "msg": "背包已满,无法容纳所购商品!"}
|
|
|
|
# 扣费并放入物品
|
|
player_data["gold"] = current_gold - price
|
|
inventory[empty_slot] = {
|
|
"vnum": goods["vnum"],
|
|
"name": goods["name"],
|
|
"count": goods.get("count", 1)
|
|
}
|
|
|
|
return {
|
|
"ok": true,
|
|
"item": inventory[empty_slot],
|
|
"slot": empty_slot,
|
|
"remaining_gold": player_data["gold"],
|
|
"msg": "成功购买【%s】!" % goods["name"]
|
|
}
|
|
|
|
# 关闭随身服务窗口
|
|
func close_service() -> void:
|
|
active_service = ""
|
|
|
|
# 内部通用道具消耗处理
|
|
func _consume_portable_item(slot_idx: int, inventory: Array, expected_vnum: int) -> Dictionary:
|
|
if slot_idx < 0 or slot_idx >= inventory.size():
|
|
return {"ok": false, "reason": "INVALID_SLOT"}
|
|
|
|
var item = inventory[slot_idx]
|
|
if item == null or int(item.get("vnum", 0)) != expected_vnum:
|
|
return {"ok": false, "reason": "ITEM_MISMATCH", "msg": "该格子上不是对应的随身卷轴!"}
|
|
|
|
var cnt: int = int(item.get("count", 1))
|
|
var remaining: int = cnt - 1
|
|
if remaining <= 0:
|
|
inventory[slot_idx] = null
|
|
else:
|
|
item["count"] = remaining
|
|
|
|
portable_item_consumed.emit(expected_vnum, remaining)
|
|
|
|
return {"ok": true, "remaining": remaining}
|