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

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}