# offline_bazaar_market_system.gd —— Metin2 40250 单人单机广场离线摆摊与虚拟交易市场 1:1 # 对照 40250 服务端 char_shop.cpp, shop.cpp, shop_manager.cpp class_name OfflineBazaarMarketSystem extends RefCounted signal stall_browsed(stall_id: int, stall_name: String) signal item_bought_from_stall(stall_id: int, item_name: String, count: int, total_price: int) signal player_stall_opened(title: String, item_count: int) signal player_stall_item_sold(item_name: String, count: int, price: int) signal player_stall_closed(revenue_collected: int) const ITEM_BUNDLE_VNUM := 50200 # 摆摊包袱 (Bundle) # 广场内置 3 大经典 NPC/虚拟玩家货摊 var npc_stalls: Dictionary = { 1: { "id": 1, "owner": "阿隆", "title": "【稀缺材料专营店】", "pos": Vector3(12.0, 0.0, 8.0), "items": [ {"vnum": 27993, "name": "珍珠", "price": 500000, "stock": 5}, {"vnum": 27992, "name": "白石", "price": 300000, "stock": 10}, {"vnum": 30040, "name": "蜘蛛眼", "price": 50000, "stock": 20}, {"vnum": 50513, "name": "灵魂之石", "price": 1200000, "stock": 3} ] }, 2: { "id": 2, "owner": "铁峰", "title": "【+4 强化灵石阁】", "pos": Vector3(18.0, 0.0, 8.0), "items": [ {"vnum": 28430, "name": "战士灵石+4", "price": 800000, "stock": 4}, {"vnum": 28431, "name": "忍者灵石+4", "price": 800000, "stock": 4}, {"vnum": 28432, "name": "修罗灵石+4", "price": 800000, "stock": 4}, {"vnum": 28435, "name": "魔石灵石+4", "price": 1500000, "stock": 2} ] }, 3: { "id": 3, "owner": "云游仙商", "title": "【珍品洗炼附魔阁】", "pos": Vector3(15.0, 0.0, 14.0), "items": [ {"vnum": 71084, "name": "属性改变秘籍", "price": 2000000, "stock": 5}, {"vnum": 71085, "name": "添加属性秘籍", "price": 1500000, "stock": 5}, {"vnum": 70024, "name": "祝福宝珠", "price": 3000000, "stock": 2}, {"vnum": 71003, "name": "祝福卷轴", "price": 600000, "stock": 15} ] } } # 玩家自己开设的离线个人商摊 var player_stall: Dictionary = { "is_open": false, "title": "", "items": [], # Array of { "vnum", "name", "count", "price" } "revenue": 0, "total_sales_count": 0 } # 获取所有广场摊位列表 func get_all_stalls() -> Array: var list: Array = [] for sid in npc_stalls.keys(): var s = npc_stalls[sid] list.append({ "id": s["id"], "owner": s["owner"], "title": s["title"], "pos": s["pos"], "item_count": s["items"].size() }) if player_stall["is_open"]: list.append({ "id": 999, "owner": "玩家自己", "title": player_stall["title"], "pos": Vector3(0.0, 0.0, 0.0), "item_count": player_stall["items"].size() }) return list # 浏览特定摊位商品 func browse_stall(stall_id: int) -> Dictionary: if not npc_stalls.has(stall_id): if stall_id == 999 and player_stall["is_open"]: stall_browsed.emit(999, player_stall["title"]) return {"ok": true, "stall": player_stall} return {"ok": false, "reason": "STALL_NOT_FOUND", "msg": "未找到该摊位!"} var stall = npc_stalls[stall_id] stall_browsed.emit(stall_id, stall["title"]) return { "ok": true, "id": stall_id, "owner": stall["owner"], "title": stall["title"], "items": stall["items"] } # 从 NPC/虚拟摊位购买物品 func buy_from_stall(stall_id: int, item_idx: int, count: int, player_data: Dictionary, inventory: Array) -> Dictionary: if not npc_stalls.has(stall_id): return {"ok": false, "reason": "STALL_NOT_FOUND", "msg": "未找到该摊位!"} var stall = npc_stalls[stall_id] var items: Array = stall["items"] if item_idx < 0 or item_idx >= items.size(): return {"ok": false, "reason": "ITEM_INDEX_INVALID", "msg": "无效的商品索引!"} var target_goods = items[item_idx] var available_stock: int = int(target_goods.get("stock", 0)) if available_stock < count: return {"ok": false, "reason": "OUT_OF_STOCK", "msg": "库存不足!剩余: %d" % available_stock} var total_cost: int = int(target_goods["price"]) * count var current_gold: int = int(player_data.get("gold", 0)) if current_gold < total_cost: return {"ok": false, "reason": "NOT_ENOUGH_GOLD", "msg": "金币不足!需要: %d, 当前: %d" % [total_cost, current_gold]} # 检查背包空位 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 - total_cost target_goods["stock"] = available_stock - count # 入包 var new_item: Dictionary = { "vnum": target_goods["vnum"], "name": target_goods["name"], "count": count } inventory[empty_slot] = new_item item_bought_from_stall.emit(stall_id, target_goods["name"], count, total_cost) return { "ok": true, "item": new_item, "slot": empty_slot, "cost": total_cost, "remaining_gold": player_data["gold"], "msg": "成功购买【%s】x%d,花费 %d 金币!" % [target_goods["name"], count, total_cost] } # 玩家开设自己的离线摆摊 # items_to_sell: Array of { "slot": int, "price": int, "count": int } func open_player_stall(title: String, items_to_sell: Array, inventory: Array, require_bundle: bool = true) -> Dictionary: if player_stall["is_open"]: return {"ok": false, "reason": "ALREADY_HAS_STALL", "msg": "你已开设了一个摊位!"} if items_to_sell.is_empty(): return {"ok": false, "reason": "NO_ITEMS_TO_SELL", "msg": "请至少上架一件物品!"} # 摆摊包袱校验 (50200) var bundle_slot = -1 if require_bundle: for i in range(inventory.size()): if inventory[i] != null and int(inventory[i].get("vnum", 0)) == ITEM_BUNDLE_VNUM: bundle_slot = i break if bundle_slot == -1: return {"ok": false, "reason": "NEED_BUNDLE_ITEM", "msg": "需要【摆摊包袱】(50200) 才能在广场开设个人货摊!"} # 收集上架物品并从背包移出 var stall_items: Array = [] for spec in items_to_sell: var slot_id: int = int(spec["slot"]) var price: int = int(spec["price"]) var sell_count: int = int(spec.get("count", 1)) if slot_id < 0 or slot_id >= inventory.size() or inventory[slot_id] == null: continue var inv_item: Dictionary = inventory[slot_id] var cur_count: int = int(inv_item.get("count", 1)) if sell_count >= cur_count: # 全量上架 var listed_item = inv_item.duplicate(true) listed_item["price"] = price stall_items.append(listed_item) inventory[slot_id] = null else: # 部分上架 var listed_item = inv_item.duplicate(true) listed_item["count"] = sell_count listed_item["price"] = price stall_items.append(listed_item) inv_item["count"] = cur_count - sell_count if stall_items.is_empty(): return {"ok": false, "reason": "NO_VALID_ITEMS", "msg": "没有有效可上架的物品!"} # 消耗摆摊包袱 if bundle_slot != -1: var b_item = inventory[bundle_slot] var b_cnt = int(b_item.get("count", 1)) if b_cnt > 1: b_item["count"] = b_cnt - 1 else: inventory[bundle_slot] = null player_stall["is_open"] = true player_stall["title"] = title if not title.strip_edges().is_empty() else "【离线便民杂货店】" player_stall["items"] = stall_items player_stall["revenue"] = 0 player_stall["total_sales_count"] = 0 player_stall_opened.emit(player_stall["title"], stall_items.size()) return { "ok": true, "title": player_stall["title"], "items_count": stall_items.size(), "msg": "个人摊位【%s】开设成功!上架了 %d 件珍品。" % [player_stall["title"], stall_items.size()] } # 模拟单机挂机离线交易成交 func simulate_offline_sales(elapsed_minutes: float) -> Dictionary: if not player_stall["is_open"] or player_stall["items"].is_empty(): return {"sold_count": 0, "earned_gold": 0} var sold_items: Array = [] var earned: int = 0 var remaining_items: Array = [] # 根据挂机时长计算可能成交的件数 var max_potential_sales = int(elapsed_minutes / 5.0) + 1 var actual_sales = 0 # 随离线时长提升成交几率(满30分钟保底成交) var sell_prob = clampf(0.5 + (elapsed_minutes / 30.0) * 0.5, 0.0, 1.0) for item in player_stall["items"]: if actual_sales < max_potential_sales and randf() <= sell_prob: actual_sales += 1 var item_rev = int(item["price"]) * int(item.get("count", 1)) earned += item_rev player_stall["revenue"] += item_rev player_stall["total_sales_count"] += 1 sold_items.append(item) player_stall_item_sold.emit(item["name"], int(item.get("count", 1)), item_rev) else: remaining_items.append(item) player_stall["items"] = remaining_items return { "sold_count": sold_items.size(), "earned_gold": earned, "remaining_items": remaining_items.size() } # 领取摊位赚取的金币 func collect_stall_revenue(player_data: Dictionary) -> Dictionary: if not player_stall["is_open"]: return {"ok": false, "reason": "NO_ACTIVE_STALL"} var rev: int = player_stall["revenue"] if rev <= 0: return {"ok": false, "reason": "NO_REVENUE", "msg": "当前没有可领取的摊位营收!"} player_data["gold"] = int(player_data.get("gold", 0)) + rev player_stall["revenue"] = 0 return { "ok": true, "collected_gold": rev, "current_gold": player_data["gold"], "msg": "成功领取摊位营业收入 %d 金币!" % rev } # 收摊并退回未售出物品 func close_player_stall(inventory: Array, player_data: Dictionary) -> Dictionary: if not player_stall["is_open"]: return {"ok": false, "reason": "NO_ACTIVE_STALL"} var rev: int = player_stall["revenue"] if rev > 0: player_data["gold"] = int(player_data.get("gold", 0)) + rev player_stall["revenue"] = 0 var returned_count: int = 0 for item in player_stall["items"]: # 寻空位退回 for i in range(inventory.size()): if inventory[i] == null: inventory[i] = item returned_count += 1 break player_stall["is_open"] = false player_stall["items"] = [] player_stall_closed.emit(rev) return { "ok": true, "revenue_collected": rev, "returned_items": returned_count, "msg": "已顺利收摊!营业额 %d 金币已汇入背包,%d 件未售物品已退回。" % [rev, returned_count] }