dankgrinder 5.16.0 → 5.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands/farm.js +1540 -0
- package/lib/commands/farmVision.js +437 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/inventory.js +49 -32
- package/lib/commands/shop.js +166 -63
- package/lib/commands/utils.js +70 -2
- package/lib/grinder.js +25 -5
- package/package.json +1 -1
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Farm Vision — image-based 3x3 farm grid analysis.
|
|
3
|
+
*
|
|
4
|
+
* Similar to fishVision, this computes per-cell features and classifies each
|
|
5
|
+
* tile with score + confidence.
|
|
6
|
+
*
|
|
7
|
+
* States:
|
|
8
|
+
* - tilled : mostly brown soil
|
|
9
|
+
* - wet : blue/dark wet soil signature
|
|
10
|
+
* - planted : strong green crop signature
|
|
11
|
+
* - unknown : low-confidence fallback
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const sharp = require('sharp');
|
|
15
|
+
const https = require('https');
|
|
16
|
+
const http = require('http');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
function downloadImage(url) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const proto = url.startsWith('https') ? https : http;
|
|
23
|
+
const req = proto.get(url, (res) => {
|
|
24
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
25
|
+
return downloadImage(res.headers.location).then(resolve, reject);
|
|
26
|
+
}
|
|
27
|
+
const chunks = [];
|
|
28
|
+
res.on('data', c => chunks.push(c));
|
|
29
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
30
|
+
res.on('error', reject);
|
|
31
|
+
});
|
|
32
|
+
req.on('error', reject);
|
|
33
|
+
req.setTimeout(12000, () => { req.destroy(); reject(new Error('farm image download timeout')); });
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function extractImageUrlFromComponents(components) {
|
|
38
|
+
for (const item of components || []) {
|
|
39
|
+
if (!item) continue;
|
|
40
|
+
const direct = item?.media?.url || item?.media?.proxy_url || item?.media?.proxyURL;
|
|
41
|
+
if (direct) return direct;
|
|
42
|
+
|
|
43
|
+
const allItems = [
|
|
44
|
+
...(Array.isArray(item?.items) ? item.items : []),
|
|
45
|
+
...(Array.isArray(item?.data?.items) ? item.data.items : []),
|
|
46
|
+
];
|
|
47
|
+
for (const it of allItems) {
|
|
48
|
+
const u = it?.media?.url || it?.media?.proxy_url || it?.media?.proxyURL || it?.url || null;
|
|
49
|
+
if (u) return u;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (item.components) {
|
|
53
|
+
const nested = extractImageUrlFromComponents(item.components);
|
|
54
|
+
if (nested) return nested;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function extractFarmImageUrl(msg) {
|
|
61
|
+
const attachmentUrl = (() => {
|
|
62
|
+
const a = msg?.attachments;
|
|
63
|
+
if (!a) return null;
|
|
64
|
+
if (Array.isArray(a) && a[0]) return a[0].url || a[0].proxyURL || null;
|
|
65
|
+
if (typeof a.values === 'function') {
|
|
66
|
+
const first = a.values().next()?.value;
|
|
67
|
+
return first?.url || first?.proxyURL || null;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
})();
|
|
71
|
+
if (attachmentUrl) return attachmentUrl;
|
|
72
|
+
|
|
73
|
+
const embedUrl = msg?.embeds?.[0]?.image?.url || msg?.embeds?.[0]?.thumbnail?.url;
|
|
74
|
+
if (embedUrl) return embedUrl;
|
|
75
|
+
|
|
76
|
+
const cv2Url = extractImageUrlFromComponents(msg?._cv2);
|
|
77
|
+
if (cv2Url) return cv2Url;
|
|
78
|
+
|
|
79
|
+
return extractImageUrlFromComponents(msg?.components);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function colorStatsForPixel(r, g, b) {
|
|
83
|
+
const sum = r + g + b;
|
|
84
|
+
const brightness = sum / 3;
|
|
85
|
+
const greenStrong = g > r * 1.12 && g > b * 1.1;
|
|
86
|
+
const blueStrong = b > r * 1.08 && b > g * 1.02;
|
|
87
|
+
const brownish = r > g * 1.03 && g > b * 1.03 && r > 45 && g > 28 && b < 95;
|
|
88
|
+
const dark = brightness < 56;
|
|
89
|
+
const whiteish = brightness > 210 && Math.abs(r - g) < 20 && Math.abs(g - b) < 20;
|
|
90
|
+
return { brightness, greenStrong, blueStrong, brownish, dark, whiteish };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function clamp01(n) {
|
|
94
|
+
return Math.max(0, Math.min(1, n));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function classifyCell(features) {
|
|
98
|
+
const { greenPct, bluePct, brownPct, darkPct, avgBrightness } = features;
|
|
99
|
+
|
|
100
|
+
// Score bands tuned for Dank farm palette.
|
|
101
|
+
const scorePlanted =
|
|
102
|
+
(greenPct * 1.9) +
|
|
103
|
+
(clamp01((120 - avgBrightness) / 120) * 0.2) -
|
|
104
|
+
(bluePct * 0.4);
|
|
105
|
+
|
|
106
|
+
const scoreWet =
|
|
107
|
+
(bluePct * 1.7) +
|
|
108
|
+
(darkPct * 0.7) +
|
|
109
|
+
(clamp01((105 - avgBrightness) / 105) * 0.15) -
|
|
110
|
+
(greenPct * 0.35);
|
|
111
|
+
|
|
112
|
+
const scoreTilled =
|
|
113
|
+
(brownPct * 1.9) +
|
|
114
|
+
(clamp01((125 - avgBrightness) / 125) * 0.1) -
|
|
115
|
+
(greenPct * 0.25) -
|
|
116
|
+
(bluePct * 0.2);
|
|
117
|
+
|
|
118
|
+
const scored = [
|
|
119
|
+
['planted', scorePlanted],
|
|
120
|
+
['wet', scoreWet],
|
|
121
|
+
['tilled', scoreTilled],
|
|
122
|
+
].sort((a, b) => b[1] - a[1]);
|
|
123
|
+
|
|
124
|
+
const [bestState, bestScore] = scored[0];
|
|
125
|
+
const secondScore = scored[1][1];
|
|
126
|
+
const margin = bestScore - secondScore;
|
|
127
|
+
|
|
128
|
+
// Confidence from score margin + minimum signal level.
|
|
129
|
+
const signal = Math.max(greenPct, bluePct, brownPct);
|
|
130
|
+
const confidence = clamp01((margin * 2.1) + (signal * 0.6));
|
|
131
|
+
|
|
132
|
+
// Low-signal cells become unknown.
|
|
133
|
+
if (signal < 0.085 || confidence < 0.20) {
|
|
134
|
+
return { state: 'unknown', confidence: +confidence.toFixed(3), scores: { planted: +scorePlanted.toFixed(3), wet: +scoreWet.toFixed(3), tilled: +scoreTilled.toFixed(3) } };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
state: bestState,
|
|
139
|
+
confidence: +confidence.toFixed(3),
|
|
140
|
+
scores: {
|
|
141
|
+
planted: +scorePlanted.toFixed(3),
|
|
142
|
+
wet: +scoreWet.toFixed(3),
|
|
143
|
+
tilled: +scoreTilled.toFixed(3),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
|
|
149
|
+
const { data, info } = await sharp(imgBuffer).raw().toBuffer({ resolveWithObject: true });
|
|
150
|
+
const { width, height, channels } = info;
|
|
151
|
+
const cellW = Math.floor(width / cols);
|
|
152
|
+
const cellH = Math.floor(height / rows);
|
|
153
|
+
|
|
154
|
+
const cells = [];
|
|
155
|
+
for (let row = 0; row < rows; row++) {
|
|
156
|
+
for (let col = 0; col < cols; col++) {
|
|
157
|
+
const sx = col * cellW;
|
|
158
|
+
const sy = row * cellH;
|
|
159
|
+
const ex = Math.min(sx + cellW, width);
|
|
160
|
+
const ey = Math.min(sy + cellH, height);
|
|
161
|
+
|
|
162
|
+
// Sample center region only (ignore grass borders around each tile).
|
|
163
|
+
const padX = Math.floor((ex - sx) * 0.18);
|
|
164
|
+
const padY = Math.floor((ey - sy) * 0.18);
|
|
165
|
+
const csx = sx + padX;
|
|
166
|
+
const csy = sy + padY;
|
|
167
|
+
const cex = ex - padX;
|
|
168
|
+
const cey = ey - padY;
|
|
169
|
+
|
|
170
|
+
let total = 0;
|
|
171
|
+
let greenPx = 0;
|
|
172
|
+
let bluePx = 0;
|
|
173
|
+
let brownPx = 0;
|
|
174
|
+
let darkPx = 0;
|
|
175
|
+
let whitePx = 0;
|
|
176
|
+
let brightSum = 0;
|
|
177
|
+
|
|
178
|
+
for (let y = csy; y < cey; y++) {
|
|
179
|
+
for (let x = csx; x < cex; x++) {
|
|
180
|
+
const idx = (y * width + x) * channels;
|
|
181
|
+
const r = data[idx];
|
|
182
|
+
const g = data[idx + 1];
|
|
183
|
+
const b = data[idx + 2];
|
|
184
|
+
const s = colorStatsForPixel(r, g, b);
|
|
185
|
+
total++;
|
|
186
|
+
brightSum += s.brightness;
|
|
187
|
+
if (s.greenStrong) greenPx++;
|
|
188
|
+
if (s.blueStrong) bluePx++;
|
|
189
|
+
if (s.brownish) brownPx++;
|
|
190
|
+
if (s.dark) darkPx++;
|
|
191
|
+
if (s.whiteish) whitePx++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const greenPct = greenPx / total;
|
|
196
|
+
const bluePct = bluePx / total;
|
|
197
|
+
const brownPct = brownPx / total;
|
|
198
|
+
const darkPct = darkPx / total;
|
|
199
|
+
const whitePct = whitePx / total;
|
|
200
|
+
const avgBrightness = brightSum / total;
|
|
201
|
+
|
|
202
|
+
const classified = classifyCell({ greenPct, bluePct, brownPct, darkPct, avgBrightness, whitePct });
|
|
203
|
+
|
|
204
|
+
cells.push({
|
|
205
|
+
row,
|
|
206
|
+
col,
|
|
207
|
+
state: classified.state,
|
|
208
|
+
confidence: classified.confidence,
|
|
209
|
+
scores: classified.scores,
|
|
210
|
+
greenPct: +greenPct.toFixed(3),
|
|
211
|
+
bluePct: +bluePct.toFixed(3),
|
|
212
|
+
brownPct: +brownPct.toFixed(3),
|
|
213
|
+
darkPct: +darkPct.toFixed(3),
|
|
214
|
+
whitePct: +whitePct.toFixed(3),
|
|
215
|
+
avgBrightness: +avgBrightness.toFixed(1),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const counts = { tilled: 0, wet: 0, planted: 0, unknown: 0 };
|
|
221
|
+
for (const c of cells) counts[c.state] = (counts[c.state] || 0) + 1;
|
|
222
|
+
|
|
223
|
+
const avgConfidence = cells.length > 0
|
|
224
|
+
? +(cells.reduce((s, c) => s + (c.confidence || 0), 0) / cells.length).toFixed(3)
|
|
225
|
+
: 0;
|
|
226
|
+
|
|
227
|
+
return { width, height, rows, cols, cells, counts, avgConfidence };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function gridToString(analysis) {
|
|
231
|
+
const icon = { tilled: 'T', wet: 'W', planted: 'P', unknown: '?' };
|
|
232
|
+
const rows = [];
|
|
233
|
+
for (let r = 0; r < (analysis?.rows || 0); r++) {
|
|
234
|
+
const line = [];
|
|
235
|
+
for (let c = 0; c < (analysis?.cols || 0); c++) {
|
|
236
|
+
const cell = (analysis?.cells || []).find(x => x.row === r && x.col === c);
|
|
237
|
+
line.push(icon[cell?.state || 'unknown']);
|
|
238
|
+
}
|
|
239
|
+
rows.push(line.join(' '));
|
|
240
|
+
}
|
|
241
|
+
return rows.join(' | ');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function evaluateActionNeed(actionName, analysis) {
|
|
245
|
+
const c = analysis?.counts || {};
|
|
246
|
+
const tilled = c.tilled || 0;
|
|
247
|
+
const wet = c.wet || 0;
|
|
248
|
+
const planted = c.planted || 0;
|
|
249
|
+
const unknown = c.unknown || 0;
|
|
250
|
+
const conf = analysis?.avgConfidence || 0;
|
|
251
|
+
const slots = 9;
|
|
252
|
+
|
|
253
|
+
// Avoid aggressive repeats when confidence is too low.
|
|
254
|
+
if (conf < 0.22) return false;
|
|
255
|
+
|
|
256
|
+
// Conservative rules: only request repeat if we have clear evidence.
|
|
257
|
+
if (actionName === 'hoe') {
|
|
258
|
+
return (tilled + wet + planted) < slots && unknown < 6;
|
|
259
|
+
}
|
|
260
|
+
if (actionName === 'water') {
|
|
261
|
+
// If still many plain-tilled (not watered/planted), water likely incomplete.
|
|
262
|
+
return tilled > 0 && (wet + planted) < slots;
|
|
263
|
+
}
|
|
264
|
+
if (actionName === 'plant') {
|
|
265
|
+
return planted < slots && (tilled + wet) > 0;
|
|
266
|
+
}
|
|
267
|
+
if (actionName === 'harvest') {
|
|
268
|
+
// If many planted remain, harvest may still be pending.
|
|
269
|
+
return planted > 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Fertilizer optional; do not force repeats.
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function evaluateActionScores(actionName, analysis) {
|
|
277
|
+
const c = analysis?.counts || {};
|
|
278
|
+
const slots = Math.max(1, (analysis?.rows || 3) * (analysis?.cols || 3));
|
|
279
|
+
const tilled = c.tilled || 0;
|
|
280
|
+
const wet = c.wet || 0;
|
|
281
|
+
const planted = c.planted || 0;
|
|
282
|
+
const unknown = c.unknown || 0;
|
|
283
|
+
const conf = analysis?.avgConfidence || 0;
|
|
284
|
+
|
|
285
|
+
const ratios = {
|
|
286
|
+
tilled: tilled / slots,
|
|
287
|
+
wet: wet / slots,
|
|
288
|
+
planted: planted / slots,
|
|
289
|
+
unknown: unknown / slots,
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const clamp = (n) => Math.max(0, Math.min(1, n));
|
|
293
|
+
const withConf = (base, mult = 0.25) => clamp(base * (0.82 + (conf * mult)));
|
|
294
|
+
|
|
295
|
+
let score = 0;
|
|
296
|
+
let threshold = 0.8;
|
|
297
|
+
let reason = 'ok';
|
|
298
|
+
|
|
299
|
+
if (actionName === 'hoe') {
|
|
300
|
+
// Hoe is considered good if farm has mostly actionable soil/crops and little unknown noise.
|
|
301
|
+
const base = (ratios.tilled * 0.9) + (ratios.wet * 0.52) + (ratios.planted * 0.4) - (ratios.unknown * 0.32);
|
|
302
|
+
score = withConf(base);
|
|
303
|
+
threshold = 0.68;
|
|
304
|
+
if (ratios.unknown > 0.45) reason = 'too_many_unknown_cells';
|
|
305
|
+
else if (ratios.tilled < 0.45 && (ratios.wet + ratios.planted) < 0.35) reason = 'not_enough_tilled_or_progressed_tiles';
|
|
306
|
+
} else if (actionName === 'water') {
|
|
307
|
+
// After watering, we want wet/planted dominance and minimal plain-tilled.
|
|
308
|
+
const base = (ratios.wet * 0.92) + (ratios.planted * 0.34) - (ratios.tilled * 0.48) - (ratios.unknown * 0.22);
|
|
309
|
+
score = withConf(base);
|
|
310
|
+
threshold = 0.7;
|
|
311
|
+
if (ratios.planted >= 0.45) {
|
|
312
|
+
// If many planted tiles are already visible, watering phase is effectively done.
|
|
313
|
+
score = Math.max(score, withConf(0.86));
|
|
314
|
+
reason = 'progressed_to_plant_phase';
|
|
315
|
+
}
|
|
316
|
+
if (ratios.wet < 0.45 && ratios.planted < 0.2) reason = 'not_enough_wet_tiles';
|
|
317
|
+
else if (ratios.tilled > 0.4) reason = 'too_many_dry_tilled_tiles';
|
|
318
|
+
} else if (actionName === 'plant') {
|
|
319
|
+
// Plant stage should end with mostly planted tiles.
|
|
320
|
+
const base = (ratios.planted * 0.96) + (ratios.wet * 0.12) - (ratios.tilled * 0.48) - (ratios.unknown * 0.2);
|
|
321
|
+
score = withConf(base);
|
|
322
|
+
threshold = 0.72;
|
|
323
|
+
if (ratios.planted < 0.6) reason = 'not_enough_planted_tiles';
|
|
324
|
+
} else if (actionName === 'harvest') {
|
|
325
|
+
// Harvest should reduce planted tiles; tilled/wet should dominate after collection.
|
|
326
|
+
const base = ((ratios.tilled + ratios.wet) * 0.86) - (ratios.planted * 0.76) - (ratios.unknown * 0.18);
|
|
327
|
+
score = withConf(base);
|
|
328
|
+
threshold = 0.68;
|
|
329
|
+
if (ratios.planted > 0.34) reason = 'too_many_unharvested_plants';
|
|
330
|
+
} else {
|
|
331
|
+
score = withConf(0.7 - (ratios.unknown * 0.2));
|
|
332
|
+
threshold = 0.7;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const matched = conf >= 0.2 && score >= threshold;
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
action: actionName,
|
|
339
|
+
matched,
|
|
340
|
+
score: +score.toFixed(3),
|
|
341
|
+
threshold: +threshold.toFixed(3),
|
|
342
|
+
confidence: +conf.toFixed(3),
|
|
343
|
+
reason,
|
|
344
|
+
counts: { tilled, wet, planted, unknown, slots },
|
|
345
|
+
ratios: {
|
|
346
|
+
tilled: +ratios.tilled.toFixed(3),
|
|
347
|
+
wet: +ratios.wet.toFixed(3),
|
|
348
|
+
planted: +ratios.planted.toFixed(3),
|
|
349
|
+
unknown: +ratios.unknown.toFixed(3),
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function dumpFarmVisionDebug({
|
|
355
|
+
imgBuffer,
|
|
356
|
+
analysis,
|
|
357
|
+
actionName = 'unknown',
|
|
358
|
+
sourceUrl = null,
|
|
359
|
+
}) {
|
|
360
|
+
const root = path.resolve(__dirname, '../../tmp-cv2-dumps/farm-vision');
|
|
361
|
+
const stamp = `${Date.now()}-${actionName}`;
|
|
362
|
+
const dir = path.join(root, stamp);
|
|
363
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
364
|
+
|
|
365
|
+
const sourcePath = path.join(dir, 'source.png');
|
|
366
|
+
await sharp(imgBuffer).png().toFile(sourcePath);
|
|
367
|
+
|
|
368
|
+
const cols = analysis?.cols || 3;
|
|
369
|
+
const rows = analysis?.rows || 3;
|
|
370
|
+
const width = analysis?.width || 0;
|
|
371
|
+
const height = analysis?.height || 0;
|
|
372
|
+
const cellW = Math.floor(width / cols);
|
|
373
|
+
const cellH = Math.floor(height / rows);
|
|
374
|
+
|
|
375
|
+
const tiles = [];
|
|
376
|
+
for (let row = 0; row < rows; row++) {
|
|
377
|
+
for (let col = 0; col < cols; col++) {
|
|
378
|
+
const sx = col * cellW;
|
|
379
|
+
const sy = row * cellH;
|
|
380
|
+
const ex = Math.min(sx + cellW, width);
|
|
381
|
+
const ey = Math.min(sy + cellH, height);
|
|
382
|
+
const w = Math.max(1, ex - sx);
|
|
383
|
+
const h = Math.max(1, ey - sy);
|
|
384
|
+
|
|
385
|
+
const cell = (analysis?.cells || []).find(c => c.row === row && c.col === col) || null;
|
|
386
|
+
const state = cell?.state || 'unknown';
|
|
387
|
+
const conf = Number.isFinite(cell?.confidence) ? cell.confidence : 0;
|
|
388
|
+
const tileName = `tile-r${row + 1}-c${col + 1}-${state}-c${String(conf).replace('.', '_')}.png`;
|
|
389
|
+
const tilePath = path.join(dir, tileName);
|
|
390
|
+
|
|
391
|
+
await sharp(imgBuffer)
|
|
392
|
+
.extract({ left: sx, top: sy, width: w, height: h })
|
|
393
|
+
.png()
|
|
394
|
+
.toFile(tilePath);
|
|
395
|
+
|
|
396
|
+
tiles.push({
|
|
397
|
+
row,
|
|
398
|
+
col,
|
|
399
|
+
state,
|
|
400
|
+
confidence: conf,
|
|
401
|
+
path: tilePath,
|
|
402
|
+
bbox: { x: sx, y: sy, w, h },
|
|
403
|
+
features: cell || null,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const manifestPath = path.join(dir, 'manifest.json');
|
|
409
|
+
const manifest = {
|
|
410
|
+
createdAt: new Date().toISOString(),
|
|
411
|
+
actionName,
|
|
412
|
+
sourceUrl,
|
|
413
|
+
image: { width, height, rows, cols },
|
|
414
|
+
counts: analysis?.counts || null,
|
|
415
|
+
avgConfidence: analysis?.avgConfidence ?? null,
|
|
416
|
+
sourcePath,
|
|
417
|
+
tiles,
|
|
418
|
+
};
|
|
419
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
dir,
|
|
423
|
+
sourcePath,
|
|
424
|
+
manifestPath,
|
|
425
|
+
tileCount: tiles.length,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
module.exports = {
|
|
430
|
+
downloadImage,
|
|
431
|
+
extractFarmImageUrl,
|
|
432
|
+
analyzeFarmGrid,
|
|
433
|
+
gridToString,
|
|
434
|
+
evaluateActionNeed,
|
|
435
|
+
evaluateActionScores,
|
|
436
|
+
dumpFarmVisionDebug,
|
|
437
|
+
};
|
package/lib/commands/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const { runBeg } = require('./beg');
|
|
|
8
8
|
const { runSearch, SAFE_SEARCH_LOCATIONS } = require('./search');
|
|
9
9
|
const { runCrime, SAFE_CRIME_OPTIONS } = require('./crime');
|
|
10
10
|
const { runHighLow } = require('./highlow');
|
|
11
|
+
const { runFarm } = require('./farm');
|
|
11
12
|
const { runHunt } = require('./hunt');
|
|
12
13
|
const { runDig } = require('./dig');
|
|
13
14
|
const { runFish, sellAllFish } = require('./fish');
|
|
@@ -32,6 +33,7 @@ module.exports = {
|
|
|
32
33
|
runSearch,
|
|
33
34
|
runCrime,
|
|
34
35
|
runHighLow,
|
|
36
|
+
runFarm,
|
|
35
37
|
runHunt,
|
|
36
38
|
runDig,
|
|
37
39
|
runFish,
|
|
@@ -218,10 +218,13 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
218
218
|
const allItems = [];
|
|
219
219
|
let { page, total } = parsePageInfo(response);
|
|
220
220
|
LOG.info(`[inv] Page ${page}/${total}`);
|
|
221
|
+
const visitedPages = new Set([page]);
|
|
221
222
|
|
|
222
223
|
allItems.push(...parseInventoryPage(response));
|
|
223
224
|
|
|
224
|
-
|
|
225
|
+
let guard = 0;
|
|
226
|
+
while (page < total && guard < Math.max(20, total + 6)) {
|
|
227
|
+
guard++;
|
|
225
228
|
const buttons = getAllButtons(response);
|
|
226
229
|
const enabled = buttons.filter(b => !b.disabled);
|
|
227
230
|
|
|
@@ -231,13 +234,43 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
231
234
|
return m ? parseInt(m[1], 10) : null;
|
|
232
235
|
};
|
|
233
236
|
|
|
234
|
-
|
|
235
|
-
const id = String(b.customId || '');
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return
|
|
237
|
+
const classifyButton = (b) => {
|
|
238
|
+
const id = String(b.customId || '').toLowerCase();
|
|
239
|
+
const label = String(b.label || '').toLowerCase();
|
|
240
|
+
const emoji = String(b.emoji?.name || '').toLowerCase();
|
|
241
|
+
return { id, label, emoji };
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const directNext = enabled.find((b) => {
|
|
245
|
+
const { id, label, emoji } = classifyButton(b);
|
|
246
|
+
if (id.includes('last') || label === '⏭' || emoji.includes('doubleright')) return false;
|
|
247
|
+
return id.includes('next')
|
|
248
|
+
|| label.includes('next')
|
|
249
|
+
|| label === '▶'
|
|
250
|
+
|| label === '→'
|
|
251
|
+
|| emoji.includes('arrowright');
|
|
239
252
|
});
|
|
240
253
|
|
|
254
|
+
let nextBtn = directNext || null;
|
|
255
|
+
|
|
256
|
+
if (!nextBtn) {
|
|
257
|
+
const paginatorCandidates = enabled
|
|
258
|
+
.map((b) => ({ b, id: String(b.customId || ''), target: parseTargetPage(b.customId || '') }))
|
|
259
|
+
.filter((x) => /paginator-inventory-list/i.test(x.id) && /setpage/i.test(x.id) && Number.isInteger(x.target));
|
|
260
|
+
|
|
261
|
+
// Prefer the smallest target greater than current page.
|
|
262
|
+
// Fallback to target===current page for legacy/offset paginator ids.
|
|
263
|
+
const gtCurrent = paginatorCandidates
|
|
264
|
+
.filter(x => x.target > page)
|
|
265
|
+
.sort((a, b) => a.target - b.target);
|
|
266
|
+
if (gtCurrent.length > 0) {
|
|
267
|
+
nextBtn = gtCurrent[0].b;
|
|
268
|
+
} else {
|
|
269
|
+
const eqCurrent = paginatorCandidates.find(x => x.target === page);
|
|
270
|
+
if (eqCurrent) nextBtn = eqCurrent.b;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
241
274
|
if (!nextBtn) {
|
|
242
275
|
nextBtn = enabled.find((b) => {
|
|
243
276
|
const id = (b.customId || '').toLowerCase();
|
|
@@ -256,35 +289,13 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
256
289
|
|
|
257
290
|
await humanDelay(200, 400);
|
|
258
291
|
|
|
259
|
-
// Set up messageUpdate listener BEFORE clicking so we don't miss the update
|
|
260
|
-
const msgId = response.id;
|
|
261
|
-
const updatePromise = new Promise((resolve) => {
|
|
262
|
-
const timeout = setTimeout(() => {
|
|
263
|
-
client.removeListener('messageUpdate', handler);
|
|
264
|
-
resolve(null);
|
|
265
|
-
}, 10000);
|
|
266
|
-
const handler = (_, m) => {
|
|
267
|
-
if (m.id === msgId) {
|
|
268
|
-
clearTimeout(timeout);
|
|
269
|
-
client.removeListener('messageUpdate', handler);
|
|
270
|
-
resolve(m);
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
client.on('messageUpdate', handler);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
292
|
try {
|
|
277
|
-
await safeClickButton(response, nextBtn);
|
|
293
|
+
const clicked = await safeClickButton(response, nextBtn);
|
|
294
|
+
if (clicked) response = clicked;
|
|
278
295
|
} catch {
|
|
279
296
|
break;
|
|
280
297
|
}
|
|
281
298
|
|
|
282
|
-
// Wait for actual message update from Discord (page content change)
|
|
283
|
-
const updated = await updatePromise;
|
|
284
|
-
if (updated) {
|
|
285
|
-
response = updated;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
299
|
// Force-refetch CV2 with retries until page actually changes
|
|
289
300
|
delete response._cv2;
|
|
290
301
|
delete response._cv2text;
|
|
@@ -293,12 +304,18 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
293
304
|
let pageChanged = false;
|
|
294
305
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
295
306
|
await sleep(attempt === 0 ? 600 : 1200);
|
|
307
|
+
try {
|
|
308
|
+
const fresh = await channel.messages.fetch(response.id);
|
|
309
|
+
if (fresh) response = fresh;
|
|
310
|
+
} catch {}
|
|
296
311
|
if (isCV2(response)) await ensureCV2(response, true);
|
|
297
312
|
const pageInfo = parsePageInfo(response);
|
|
298
|
-
if (pageInfo.page > page) {
|
|
313
|
+
if (pageInfo.page > page && !visitedPages.has(pageInfo.page)) {
|
|
299
314
|
page = pageInfo.page;
|
|
300
315
|
total = pageInfo.total;
|
|
316
|
+
visitedPages.add(page);
|
|
301
317
|
pageChanged = true;
|
|
318
|
+
LOG.info(`[inv] Page ${page}/${total}`);
|
|
302
319
|
break;
|
|
303
320
|
}
|
|
304
321
|
// Clear CV2 cache again for next retry
|
|
@@ -323,7 +340,7 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
323
340
|
}
|
|
324
341
|
|
|
325
342
|
const items = Object.values(itemMap);
|
|
326
|
-
LOG.success(`[inv] Found ${items.length} unique items across ${total} pages`);
|
|
343
|
+
LOG.success(`[inv] Found ${items.length} unique items across ${visitedPages.size}/${total} pages`);
|
|
327
344
|
|
|
328
345
|
const { totalValue, totalMarket } = await enrichItems(items);
|
|
329
346
|
LOG.info(`[inv] Net value: ${c.bold}${c.green}⏣ ${totalValue.toLocaleString()}${c.reset} Market: ${c.bold}⏣ ${totalMarket.toLocaleString()}${c.reset}`);
|