feat: initial commit for TheFarmer project
This commit is contained in:
322
211/server/src/core/WarehouseManager.js
Normal file
322
211/server/src/core/WarehouseManager.js
Normal file
@@ -0,0 +1,322 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user