feat: initial commit for TheFarmer project

This commit is contained in:
Karriis
2026-02-18 13:52:06 +08:00
commit 8ceb5fa9db
420 changed files with 61918 additions and 0 deletions

View 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 };