Files
Farmer/211/server/src/core/WarehouseManager.js
2026-02-18 13:52:06 +08:00

323 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };