# mysterious_merchant_blackmarket_system.gd —— Metin2 40250 野外随机神秘游商与黑市折扣市场系统 1:1 # 对照 40250 服务端 shop.cpp, regen.cpp, char_item.cpp class_name MysteriousMerchantBlackmarketSystem extends RefCounted signal merchant_appeared(map_name: String, pos: Vector2, duration_seconds: float) signal merchant_departed() signal item_bought(item_name: String, cost: int) signal item_sold(item_name: String, earned: int) signal stock_refreshed(stock_list: Array) const NPC_VNUM := 20095 # 野外神秘游商 (Wandering Merchant) const DEFAULT_DURATION := 1800.0 # 停留 30 分钟 (1800 秒) const HIGH_PRICE_BUYBACK_MULTIPLIER := 1.3 # 高价回收 +30% 黄金溢价 (1.30x) const ITEM_ANTIFLAG_SELL := 1 << 8 # 256 禁售标志位 # 3 大野外候选刷新点 (沙漠绿洲、雪山孤峰、烈焰核心) const SPAWN_LOCATIONS: Array = [ {"map": "Desert", "name": "沙漠绿洲", "pos": Vector2(120, 350)}, {"map": "Mount_Sohan", "name": "雪山孤峰", "pos": Vector2(450, 680)}, {"map": "Doyyumhwan", "name": "烈焰核心", "pos": Vector2(600, 210)} ] # 黑市半价珍品底库预设 (40250 官方核心珍宝) const DEFAULT_BLACK_MARKET_POOL: Array = [ { "vnum": 51501, "name": "龙石炼金原石(巨龙之心)", "base_price": 2000000, "price": 1000000, # 5折半价 "stock": 5, "count": 1, "type": 16 # ITEM_METIN / 炼金 }, { "vnum": 28430, "name": "+4 灵石礼包", "base_price": 5000000, "price": 2500000, # 5折半价 "stock": 3, "count": 1, "type": 10 # ITEM_STONE }, { "vnum": 50300, "name": "古代进阶技能秘籍", "base_price": 3000000, "price": 1500000, # 5折半价 "stock": 3, "count": 1, "type": 17 # ITEM_SKILLBOOK }, { "vnum": 25040, "name": "洗炼祝福包(双份)", "base_price": 1000000, "price": 500000, # 5折半价 "stock": 10, "count": 2, "type": 3 # ITEM_USE }, { "vnum": 71084, "name": "传说洗练神丹", "base_price": 4000000, "price": 2000000, # 5折半价 "stock": 2, "count": 1, "type": 3 # ITEM_USE } ] # 当前游商运行状态 var is_active: bool = false var current_map: String = "" var current_pos: Vector2 = Vector2.ZERO var time_remaining: float = 0.0 var total_duration: float = DEFAULT_DURATION var current_stock: Array = [] # 随机刷出野外神秘游商 func spawn_random_merchant(duration: float = DEFAULT_DURATION, loc_index: int = -1) -> Dictionary: var loc_idx: int = loc_index if loc_idx < 0 or loc_idx >= SPAWN_LOCATIONS.size(): loc_idx = randi() % SPAWN_LOCATIONS.size() var chosen_loc: Dictionary = SPAWN_LOCATIONS[loc_idx] return spawn_merchant(chosen_loc["map"], chosen_loc["pos"], duration) # 指定地点召唤/刷出游商 func spawn_merchant(map_name: String, pos: Vector2, duration: float = DEFAULT_DURATION) -> Dictionary: is_active = true current_map = map_name current_pos = pos time_remaining = duration total_duration = duration refresh_stock() merchant_appeared.emit(current_map, current_pos, duration) return { "ok": true, "map": current_map, "pos": current_pos, "duration": duration, "npc_vnum": NPC_VNUM } # 游商离去或收摊 func despawn_merchant() -> void: if not is_active: return is_active = false current_map = "" current_pos = Vector2.ZERO time_remaining = 0.0 current_stock.clear() merchant_departed.emit() # 物理帧或逻辑心跳更新 func update(delta: float) -> void: if not is_active: return time_remaining -= delta if time_remaining <= 0.0: despawn_merchant() # 重新生成黑市货架 (重置折扣和限量库存) func refresh_stock() -> void: current_stock.clear() for item_template in DEFAULT_BLACK_MARKET_POOL: var good: Dictionary = item_template.duplicate(true) current_stock.append(good) stock_refreshed.emit(current_stock) # 获取当前商品清单 func get_stock() -> Array: return current_stock.duplicate(true) # 购买黑市商品 func buy_item(stock_idx: int, player_data: Dictionary, inventory: Array) -> Dictionary: if not is_active: return {"ok": false, "reason": "MERCHANT_NOT_ACTIVE", "msg": "神秘游商已离去或尚未现身!"} if stock_idx < 0 or stock_idx >= current_stock.size(): return {"ok": false, "reason": "INVALID_STOCK_INDEX", "msg": "商品编号无效!"} var good: Dictionary = current_stock[stock_idx] var current_qty := int(good.get("stock", 0)) if current_qty <= 0: return {"ok": false, "reason": "OUT_OF_STOCK", "msg": "该珍品黑市配额已售罄!"} var price := int(good.get("price", 0)) var player_gold := int(player_data.get("gold", 0)) if player_gold < price: return {"ok": false, "reason": "NOT_ENOUGH_GOLD", "msg": "金币不足,无法购买黑市珍品!"} # 寻找背包空槽 var target_slot := -1 for i in range(inventory.size()): if inventory[i] == null or (inventory[i] is Dictionary and inventory[i].is_empty()): target_slot = i break if target_slot == -1: return {"ok": false, "reason": "INVENTORY_FULL", "msg": "背包已满,请清理后再购买!"} # 扣费与发货 player_data["gold"] = player_gold - price good["stock"] = current_qty - 1 var bought_item: Dictionary = { "vnum": good["vnum"], "name": good["name"], "count": good.get("count", 1), "type": good.get("type", 0) } inventory[target_slot] = bought_item item_bought.emit(good["name"], price) return { "ok": true, "slot": target_slot, "item": bought_item, "cost": price, "remaining_stock": good["stock"], "msg": "成功以黑市特惠价购入 %s!" % good["name"] } # 计算高价回收金额 (+30% 溢价) func calculate_sell_price(item: Dictionary) -> int: var anti_flags := int(item.get("anti_flags", item.get("anti_flag", 0))) if anti_flags & ITEM_ANTIFLAG_SELL != 0: return 0 # 禁售品 var base_price := int(item.get("shop_buy_price", item.get("gold", item.get("price", 1000)))) var count := maxi(1, int(item.get("count", 1))) # 40250 官方标准回收价为买入价的 1/5 (20%) var standard_unit_sell: int = maxi(1, int(base_price / 5.0)) # 神秘游商提供 +30% 溢价 (1.30 倍) var blackmarket_unit_sell: int = int(round(standard_unit_sell * HIGH_PRICE_BUYBACK_MULTIPLIER)) return maxi(1, blackmarket_unit_sell * count) # 向神秘游商高价出售闲置珍品 func sell_item(inv_slot: int, player_data: Dictionary, inventory: Array) -> Dictionary: if not is_active: return {"ok": false, "reason": "MERCHANT_NOT_ACTIVE", "msg": "神秘游商已离去,无法交易!"} if inv_slot < 0 or inv_slot >= inventory.size() or inventory[inv_slot] == null: return {"ok": false, "reason": "INVALID_SLOT", "msg": "背包槽位为空或无效!"} var item: Dictionary = inventory[inv_slot] var anti_flags := int(item.get("anti_flags", item.get("anti_flag", 0))) if anti_flags & ITEM_ANTIFLAG_SELL != 0: return {"ok": false, "reason": "ITEM_ANTIFLAG_SELL", "msg": "该物品属于非卖品,无法出售!"} var earn_gold := calculate_sell_price(item) if earn_gold <= 0: return {"ok": false, "reason": "VALUATION_ZERO", "msg": "该物品估价为 0,游商不予回收!"} var item_name: String = str(item.get("name", "珍宝")) # 结算金币与移除物品 player_data["gold"] = int(player_data.get("gold", 0)) + earn_gold inventory[inv_slot] = null item_sold.emit(item_name, earn_gold) return { "ok": true, "earned": earn_gold, "item_name": item_name, "msg": "神秘游商高价收购了【%s】,支付了 %d 金币(+30%% 溢价)!" % [item_name, earn_gold] } # 存档序列化 func serialize() -> Dictionary: return { "is_active": is_active, "current_map": current_map, "pos_x": current_pos.x, "pos_y": current_pos.y, "time_remaining": time_remaining, "total_duration": total_duration, "stock": current_stock.duplicate(true) } # 读档反序列化 func deserialize(data: Dictionary) -> void: is_active = bool(data.get("is_active", false)) current_map = str(data.get("current_map", "")) current_pos = Vector2(float(data.get("pos_x", 0.0)), float(data.get("pos_y", 0.0))) time_remaining = float(data.get("time_remaining", 0.0)) total_duration = float(data.get("total_duration", DEFAULT_DURATION)) current_stock = data.get("stock", []).duplicate(true)