const protobuf = require('protobufjs'); const { types } = require('../proto'); const { toLong, toNum, log, logWarn, emitRuntimeHint } = require('../utils'); const { getFruitName, getPlantNameBySeedId, getItemNameById } = require('../gameConfig'); const seedShopData = require('../../tools/seed-shop-merged-export.json'); // 游戏内金币和点券的物品 ID (GlobalData.GodItemId / DiamondItemId) const GOLD_ITEM_ID = 1001; const FRUIT_ID_SET = new Set( ((seedShopData && seedShopData.rows) || []) .map(row => Number(row.fruitId)) .filter(Number.isFinite) ); class WarehouseManager { constructor(bot) { this.bot = bot; this.sellTimer = null; this.sellInterval = 60000; } isFruitIdBySeedData(id) { return FRUIT_ID_SET.has(toNum(id)); } /** * 从 SellReply 中提取获得的金币数量 * 新版 SellReply 返回 get_items (repeated Item),其中 id=1001 为金币 */ extractGold(sellReply) { if (sellReply.get_items && sellReply.get_items.length > 0) { for (const item of sellReply.get_items) { const id = toNum(item.id); if (id === GOLD_ITEM_ID) { return toNum(item.count); } } return 0; } if (sellReply.gold !== undefined && sellReply.gold !== null) { return toNum(sellReply.gold); } return 0; } async getBag() { const body = types.BagRequest.encode(types.BagRequest.create({})).finish(); const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'Bag', body); return types.BagReply.decode(replyBody); } /** * 将 item 转为 Sell 请求所需格式(id/count/uid 保留 Long 或转成 Long,与游戏一致) */ toSellItem(item) { const id = item.id != null ? toLong(item.id) : undefined; const count = item.count != null ? toLong(item.count) : undefined; const uid = item.uid != null ? toLong(item.uid) : undefined; return { id, count, uid }; } async sellItems(items) { const payload = items.map(this.toSellItem); const body = types.SellRequest.encode(types.SellRequest.create({ items: payload })).finish(); const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'Sell', body); return types.SellReply.decode(replyBody); } /** * 从 BagReply 取出物品列表(兼容 item_bag 与旧版 items) */ getBagItems(bagReply) { if (bagReply.item_bag && bagReply.item_bag.items && bagReply.item_bag.items.length) return bagReply.item_bag.items; return bagReply.items || []; } /** * 获取格式化的背包数据,用于前端显示 */ async getFormattedBag() { const bagReply = await this.getBag(); const items = this.getBagItems(bagReply); const seeds = []; const produce = []; const others = []; for (const item of items) { const id = toNum(item.id); const count = toNum(item.count); const uid = item.uid ? toNum(item.uid) : 0; // Basic item object const itemObj = { id, uid, count, name: `物品 ${id}`, type: 'other' }; if (this.isFruitIdBySeedData(id) || id === 40416) { itemObj.name = getFruitName(id); itemObj.type = 'produce'; produce.push(itemObj); } else { // 尝试识别为种子 const plantName = getPlantNameBySeedId(id); if (plantName && plantName !== `种子${id}`) { itemObj.name = plantName + '种子'; itemObj.type = 'seed'; seeds.push(itemObj); } else { // 过滤掉货币类物品和特殊物品 // 1001:金币, 1002:点券, 1101:种植经验, 3001:普通收藏点, 1011/1012:化肥容器 if ([1001, 1002, 1101, 3001, 1011, 1012].includes(id)) { continue; } // 其他物品 itemObj.name = getItemNameById(id); others.push(itemObj); } } } return { seeds, produce, others }; } async sellAllFruits() { if (!this.bot.network.connected) return; try { const bagReply = await this.getBag(); const items = this.getBagItems(bagReply); const toSell = []; const names = []; for (const item of items) { const id = toNum(item.id); const count = toNum(item.count); const uid = item.uid ? toNum(item.uid) : 0; if (this.isFruitIdBySeedData(id) && count > 0) { if (uid === 0) continue; // 跳过无效格子 toSell.push(item); names.push(`${getFruitName(id)} ${count} 个`); } } if (toSell.length === 0) return; const reply = await this.sellItems(toSell); const totalGold = this.extractGold(reply); log('仓库', `成功出售: ${names.join(',')}。共获得 ${totalGold} 金币`); emitRuntimeHint(false); } catch (e) { logWarn('仓库', `物品出售失败: ${e.message}`); } } async useItems(items) { if (!items || items.length === 0) return { success: true, message: '没有选择物品' }; const results = []; const fertilizers = new Set([80001, 80002, 80003, 80004, 80011, 80012, 80013, 80014]); const formatGains = (replyItems, excludeId) => { if (!replyItems || replyItems.length === 0) return ''; const parts = []; for (const replyItem of replyItems) { const gainId = toNum(replyItem.id); const gainCount = toNum(replyItem.count); if (excludeId && gainId === excludeId) continue; if (!gainId || !gainCount) continue; parts.push(`${getItemNameById(gainId)} x${gainCount}`); } return parts.join(','); }; const buildGainList = (replyItems, excludeId) => { if (!replyItems || replyItems.length === 0) return []; const gains = []; for (const replyItem of replyItems) { const gainId = toNum(replyItem.id); const gainCount = toNum(replyItem.count); if (excludeId && gainId === excludeId) continue; if (!gainId || !gainCount) continue; gains.push({ id: gainId, count: gainCount }); } return gains; }; const buildItemCountMap = (bagItems) => { const map = new Map(); for (const bagItem of bagItems) { const bagId = toNum(bagItem.id); const bagCount = toNum(bagItem.count); if (!bagId || !bagCount) continue; map.set(bagId, (map.get(bagId) || 0) + bagCount); } return map; }; const diffGains = (beforeMap, afterMap, excludeId) => { const parts = []; for (const [id, afterCount] of afterMap.entries()) { if (excludeId && id === excludeId) continue; const beforeCount = beforeMap.get(id) || 0; const delta = afterCount - beforeCount; if (delta > 0) { parts.push(`${getItemNameById(id)} x${delta}`); } } return parts.join(','); }; const diffGainList = (beforeMap, afterMap, excludeId) => { const gains = []; for (const [id, afterCount] of afterMap.entries()) { if (excludeId && id === excludeId) continue; const beforeCount = beforeMap.get(id) || 0; const delta = afterCount - beforeCount; if (delta > 0) gains.push({ id, count: delta }); } return gains; }; const mergeGains = (gainMap, gains) => { for (const gain of gains) { if (!gain || !gain.id || !gain.count) continue; gainMap.set(gain.id, (gainMap.get(gain.id) || 0) + gain.count); } }; const totalGains = new Map(); for (const item of items) { const id = Number(item.id); const count = Number(item.count); let uid = item.uid != null ? Number(item.uid) : 0; const name = getItemNameById(id); try { let usedCount = count; let gainsText = ''; let gainList = []; if (id === 100003) { const beforeReply = await this.getBag(); const beforeItems = this.getBagItems(beforeReply); const beforeMap = buildItemCountMap(beforeItems); if (!uid) { const bagReply = await this.getBag(); const bagItems = this.getBagItems(bagReply); const found = bagItems.find(bagItem => toNum(bagItem.id) === id && toNum(bagItem.uid) > 0); if (found) uid = toNum(found.uid); } if (!uid) { throw new Error('礼包缺少UID'); } const itemWriter = protobuf.Writer.create(); itemWriter.uint32(8).int64(toLong(id)); itemWriter.uint32(16).int64(toLong(count)); itemWriter.uint32(48).int64(toLong(uid)); const body = protobuf.Writer.create().uint32(10).bytes(itemWriter.finish()).finish(); await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'Use', body); const afterReply = await this.getBag(); const afterItems = this.getBagItems(afterReply); const afterMap = buildItemCountMap(afterItems); gainsText = diffGains(beforeMap, afterMap, id); gainList = diffGainList(beforeMap, afterMap, id); } else { const payload = { items: [{ item_id: toLong(id), count: toLong(count), land_count: toLong(1) }] }; const body = types.BatchUseRequest.encode(types.BatchUseRequest.create(payload)).finish(); const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'BatchUse', body); const reply = types.BatchUseReply.decode(replyBody); gainsText = formatGains(reply.items, id); gainList = buildGainList(reply.items, id); } if (fertilizers.has(id)) { results.push(`${name}: 已使用(消耗 ${usedCount} 个)${gainsText ? `,获得:${gainsText}` : ''}`); } else { results.push(`${name}: 已使用${gainsText ? `,获得:${gainsText}` : ''}`); } if (gainsText) { log('仓库', `使用 ${name} 获得:${gainsText}`); } mergeGains(totalGains, gainList); } catch (e) { logWarn('仓库', `物品使用失败 (${name}): ${e.message}`); results.push(`${name}: 使用失败 - ${e.message}`); } } const gains = []; for (const [id, count] of totalGains.entries()) { gains.push({ id, name: getItemNameById(id), count }); } return { success: true, message: results.join('; '), gains }; } startLoop(interval = 60000) { if (this.sellTimer) return; this.sellInterval = interval; // 延迟启动,避免刚上线就请求 setTimeout(() => { if (!this.bot.isRunning) return; this.sellAllFruits(); this.sellTimer = setInterval(() => this.sellAllFruits(), this.sellInterval); }, 10000); } stopLoop() { if (this.sellTimer) { clearInterval(this.sellTimer); this.sellTimer = null; } } } module.exports = { WarehouseManager };