# 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 }