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

188 lines
6.9 KiB
GDScript

# npc_shop_system.gd —— NPC 商店库存配置与买卖经济闭环(40250 官方 1:1 对齐)
# 对齐源码:
# - 40250 server/game/src/shop.cpp:450-580 (Buy, Sell, ITEM_ANTIFLAG_SELL, dwPrice /= 5)
# - 40250 root/uishop.py (ShopDialog, SetItemData, Buy, Sell)
# - 40250 uiscript/shopdialog.py (40 格商店货架)
class_name NpcShopSystem
extends RefCounted
const SHOP_HOST_ITEM_MAX_NUM := 40
const ITEM_ANTIFLAG_SELL := 1 << 8
const ITEM_FLAG_COUNT_PER_1GOLD := 1 << 3
# NPC Vnum 常量
const NPC_WEAPON_SHOP := 9001 # 武器商人
const NPC_ARMOR_SHOP := 9002 # 防具商人
const NPC_GENERAL_STORE := 9003 # 杂货商人
const NPC_FISHERMAN := 9009 # 渔夫
const NPC_DEONKBAE := 20015 # 矿工德安克贝
# 40250 官方标准商店货架预设表
const NPC_SHOPS := {
NPC_GENERAL_STORE: [
{"pos": 0, "vnum": 27001, "count": 200, "price": 1200, "name": "小型红药水"},
{"pos": 1, "vnum": 27002, "count": 200, "price": 4800, "name": "中型红药水"},
{"pos": 2, "vnum": 27003, "count": 200, "price": 16000, "name": "大型红药水"},
{"pos": 3, "vnum": 27004, "count": 200, "price": 2400, "name": "小型蓝药水"},
{"pos": 4, "vnum": 27005, "count": 200, "price": 9600, "name": "中型蓝药水"},
{"pos": 5, "vnum": 27006, "count": 200, "price": 32000, "name": "大型蓝药水"},
{"pos": 6, "vnum": 22001, "count": 1, "price": 10000, "name": "回城卷轴"},
{"pos": 7, "vnum": 22010, "count": 1, "price": 12000, "name": "地标传送卷轴"},
{"pos": 8, "vnum": 27800, "count": 50, "price": 2000, "name": "鱼饵(蚯蚓)"},
{"pos": 9, "vnum": 27400, "count": 1, "price": 50000, "name": "钓竿+0"},
{"pos": 10, "vnum": 29101, "count": 1, "price": 80000, "name": "矿镐+0"},
],
NPC_WEAPON_SHOP: [
{"pos": 0, "vnum": 10, "count": 1, "price": 1000, "name": "长剑+0"},
{"pos": 1, "vnum": 1000, "count": 1, "price": 1000, "name": "匕首+0"},
{"pos": 2, "vnum": 2000, "count": 1, "price": 1000, "name": "短弓+0"},
{"pos": 3, "vnum": 3000, "count": 1, "price": 1000, "name": "长戟+0"},
{"pos": 4, "vnum": 5000, "count": 1, "price": 1000, "name": "古铜铃+0"},
{"pos": 5, "vnum": 7000, "count": 1, "price": 1000, "name": "折扇+0"},
{"pos": 6, "vnum": 8000, "count": 200, "price": 100, "name": "木箭"},
],
NPC_ARMOR_SHOP: [
{"pos": 0, "vnum": 11200, "count": 1, "price": 1000, "name": "武士甲+0"},
{"pos": 1, "vnum": 11400, "count": 1, "price": 1000, "name": "刺客服+0"},
{"pos": 2, "vnum": 11600, "count": 1, "price": 1000, "name": "幽灵袍+0"},
{"pos": 3, "vnum": 11800, "count": 1, "price": 1000, "name": "萨满裙+0"},
{"pos": 4, "vnum": 12200, "count": 1, "price": 1000, "name": "铁盔+0"},
{"pos": 5, "vnum": 13000, "count": 1, "price": 1000, "name": "战斗盾+0"},
]
}
## 获取指定 NPC 的商店商品列表
static func get_shop_items(npc_vnum: int) -> Array:
return NPC_SHOPS.get(npc_vnum, []).duplicate(true)
## 检查 NPC 是否拥有商店
static func has_shop(npc_vnum: int) -> bool:
return NPC_SHOPS.has(npc_vnum)
## 计算物品出售折价 (40250 shop.cpp:520: dwPrice /= 5)
## 官方折价逻辑:一般物品回收价为买入价的 1/5(向上取整保底 1 金币)
static func calculate_sell_price(item_proto: Dictionary, count := 1) -> int:
var anti_flag := int(item_proto.get("anti_flags", item_proto.get("anti_flag", 0)))
if anti_flag & ITEM_ANTIFLAG_SELL != 0:
return 0 # 禁售物品不可回收
var buy_price := int(item_proto.get("shop_buy_price", item_proto.get("gold", 100)))
var flag := int(item_proto.get("flags", item_proto.get("flag", 0)))
var unit_price: int = 1
if flag & ITEM_FLAG_COUNT_PER_1GOLD != 0:
unit_price = 1
else:
unit_price = maxi(1, buy_price / 5)
return unit_price * maxi(1, count)
## 执行商店购买事务 (40250 shop.cpp Buy)
## player_ctx: {"gold": int, "inventory": Array}
static func buy_item(shop_pos: int, count: int, npc_vnum: int, player_ctx: Dictionary) -> Dictionary:
var items := get_shop_items(npc_vnum)
var target_good: Dictionary = {}
for it in items:
if int(it.get("pos", -1)) == shop_pos:
target_good = it
break
if target_good.is_empty():
return {"ok": false, "code": "SHOP_INVALID_POS", "msg": "该商品位置无效!"}
var buy_count := maxi(1, count)
var price_per_pack := int(target_good.get("price", 0))
var pack_count := int(target_good.get("count", 1))
var total_cost: int = price_per_pack * buy_count
var current_gold := int(player_ctx.get("gold", 0))
if current_gold < total_cost:
return {
"ok": false,
"code": "NOT_ENOUGH_GOLD",
"msg": "金币不足,无法购买!(需要 %d 金币,当前拥有 %d)" % [total_cost, current_gold]
}
# 检查背包剩余空位
var inv: Array = player_ctx.get("inventory", [])
var free_cell := -1
for cell in 90: # 0..89 背包槽
var occupied := false
for inv_item in inv:
if int(inv_item.get("cell", -1)) == cell:
occupied = true
break
if not occupied:
free_cell = cell
break
if free_cell == -1:
return {"ok": false, "code": "INVENTORY_FULL", "msg": "背包空间不足,无法放入商品!"}
# 扣款并给物品
player_ctx["gold"] = current_gold - total_cost
var new_item := {
"cell": free_cell,
"vnum": int(target_good.get("vnum", 0)),
"count": pack_count * buy_count,
"sockets": [0, 0, 0],
"attrs": []
}
inv.append(new_item)
player_ctx["inventory"] = inv
return {
"ok": true,
"code": "SUCCESS",
"cost": total_cost,
"cell": free_cell,
"item": new_item,
"msg": "购买成功!花费了 %d 金币。" % total_cost
}
## 执行商店出售事务 (40250 shop.cpp Sell)
## inv_cell: 背包槽位
static func sell_item(inv_cell: int, count: int, player_ctx: Dictionary, proto_getter: Callable) -> Dictionary:
var inv: Array = player_ctx.get("inventory", [])
var target_idx := -1
var target_item: Dictionary = {}
for i in inv.size():
if int(inv[i].get("cell", -1)) == inv_cell:
target_idx = i
target_item = inv[i]
break
if target_item.is_empty():
return {"ok": false, "code": "NO_ITEM", "msg": "背包该格没有物品!"}
var vnum := int(target_item.get("vnum", 0))
var proto: Dictionary = proto_getter.call(vnum) if proto_getter.is_valid() else {}
# 校验是否禁售
var anti_flag := int(proto.get("anti_flags", proto.get("anti_flag", 0)))
if anti_flag & ITEM_ANTIFLAG_SELL != 0:
return {"ok": false, "code": "ITEM_ANTIFLAG_SELL", "msg": "该物品为非卖品,无法向商人出售!"}
var cur_count := int(target_item.get("count", 1))
var sell_count := mini(cur_count, maxi(1, count))
var earn_gold := calculate_sell_price(proto, sell_count)
# 扣减物品
if cur_count <= sell_count:
inv.remove_at(target_idx)
else:
target_item["count"] = cur_count - sell_count
player_ctx["inventory"] = inv
player_ctx["gold"] = int(player_ctx.get("gold", 0)) + earn_gold
return {
"ok": true,
"code": "SUCCESS",
"gold_earned": earn_gold,
"sold_vnum": vnum,
"sold_count": sell_count,
"msg": "出售成功!获得了 %d 金币。" % earn_gold
}