dankgrinder 5.16.0 → 5.19.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/shop.js +166 -63
- package/lib/commands/utils.js +70 -2
- package/lib/grinder.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1540 @@
|
|
|
1
|
+
const {
|
|
2
|
+
LOG, c, getFullText, parseCoins, getAllButtons, getAllSelectMenus,
|
|
3
|
+
safeClickButton, logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
|
|
4
|
+
isCV2, ensureCV2, stripAnsi, needsItem, clickCV2SelectMenu,
|
|
5
|
+
} = require('./utils');
|
|
6
|
+
const { buyItem, buyItemsBatch } = require('./shop');
|
|
7
|
+
const {
|
|
8
|
+
downloadImage,
|
|
9
|
+
extractFarmImageUrl,
|
|
10
|
+
analyzeFarmGrid,
|
|
11
|
+
gridToString,
|
|
12
|
+
evaluateActionNeed,
|
|
13
|
+
evaluateActionScores,
|
|
14
|
+
dumpFarmVisionDebug,
|
|
15
|
+
} = require('./farmVision');
|
|
16
|
+
|
|
17
|
+
const RE_TS = /<t:(\d+):R>/;
|
|
18
|
+
const RE_MIN = /(\d+)\s*minute/i;
|
|
19
|
+
const RE_HR = /(\d+)\s*hour/i;
|
|
20
|
+
|
|
21
|
+
function parseFarmCooldownSec(text) {
|
|
22
|
+
const clean = String(text || '');
|
|
23
|
+
const lower = clean.toLowerCase();
|
|
24
|
+
const hasCooldownContext = /easy tiger|slow it down|already farmed|farm again|cooldown|can be used again|on cooldown/.test(lower);
|
|
25
|
+
if (!hasCooldownContext) return null;
|
|
26
|
+
|
|
27
|
+
const ts = clean.match(RE_TS);
|
|
28
|
+
if (ts) {
|
|
29
|
+
const diff = parseInt(ts[1], 10) - Math.floor(Date.now() / 1000);
|
|
30
|
+
if (diff > 0) return diff;
|
|
31
|
+
}
|
|
32
|
+
const mm = clean.match(RE_MIN);
|
|
33
|
+
if (mm) return parseInt(mm[1], 10) * 60;
|
|
34
|
+
const hh = clean.match(RE_HR);
|
|
35
|
+
if (hh) return parseInt(hh[1], 10) * 3600;
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseFarmGrowReadySec(text) {
|
|
40
|
+
const clean = String(stripAnsi(text || ''));
|
|
41
|
+
const lower = clean.toLowerCase();
|
|
42
|
+
|
|
43
|
+
// If crops are already harvestable, don't queue waiting.
|
|
44
|
+
if (/ready to harvest|harvest ready|can be harvested|wilt/.test(lower)) return 0;
|
|
45
|
+
|
|
46
|
+
const now = Math.floor(Date.now() / 1000);
|
|
47
|
+
|
|
48
|
+
// Prefer explicit Discord relative timestamps that appear on lines with "ready".
|
|
49
|
+
const tsNearReady = [];
|
|
50
|
+
const re = /<t:(\d+):R>/ig;
|
|
51
|
+
let m;
|
|
52
|
+
while ((m = re.exec(clean)) !== null) {
|
|
53
|
+
const t = parseInt(m[1], 10);
|
|
54
|
+
if (!Number.isFinite(t) || t <= now) continue;
|
|
55
|
+
const idx = m.index;
|
|
56
|
+
const left = clean.slice(Math.max(0, idx - 80), idx).toLowerCase();
|
|
57
|
+
const right = clean.slice(idx, Math.min(clean.length, idx + 30)).toLowerCase();
|
|
58
|
+
if (/ready|grow|seed/.test(left) || /ready/.test(right)) {
|
|
59
|
+
tsNearReady.push(t - now);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (tsNearReady.length > 0) return Math.max(5, Math.min(...tsNearReady));
|
|
63
|
+
|
|
64
|
+
// Fallback textual patterns.
|
|
65
|
+
const h = clean.match(/ready\s+in\s+(\d+)\s*hour/i);
|
|
66
|
+
if (h) return Math.max(5, parseInt(h[1], 10) * 3600);
|
|
67
|
+
const mi = clean.match(/ready\s+in\s+(\d+)\s*minute/i);
|
|
68
|
+
if (mi) return Math.max(5, parseInt(mi[1], 10) * 60);
|
|
69
|
+
const s = clean.match(/ready\s+in\s+(\d+)\s*second/i);
|
|
70
|
+
if (s) return Math.max(5, parseInt(s[1], 10));
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const FARM_RESTOCK_ORDER = ['hoe', 'watering can', 'seeds', 'water bucket'];
|
|
76
|
+
const FARM_BUY_ALIASES = Object.freeze({
|
|
77
|
+
'hoe': ['hoe'],
|
|
78
|
+
'watering can': ['watering can', 'water can'],
|
|
79
|
+
'water bucket': ['water bucket', 'bucket'],
|
|
80
|
+
'seeds': ['seeds', 'seed', 'corn seeds'],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const FARM_TOTAL_SLOTS = 9;
|
|
84
|
+
|
|
85
|
+
function parseMissingFarmItem(text) {
|
|
86
|
+
const lower = String(stripAnsi(text || '')).toLowerCase();
|
|
87
|
+
|
|
88
|
+
// Reuse shared detector first.
|
|
89
|
+
const genericMissing = needsItem(lower);
|
|
90
|
+
if (genericMissing === 'hoe' || genericMissing === 'watering can' || genericMissing === 'water bucket' || genericMissing === 'seeds') {
|
|
91
|
+
return genericMissing;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hasMissingHint = /(missing|need|needs|requires?|don['’]?t have|dont have|lack|you need)/i.test(lower)
|
|
95
|
+
|| lower.includes('need the following items')
|
|
96
|
+
|| lower.includes('need following items')
|
|
97
|
+
|| lower.includes('missing items');
|
|
98
|
+
if (!hasMissingHint) return null;
|
|
99
|
+
|
|
100
|
+
if (lower.includes('watering can') || lower.includes('water can')) return 'watering can';
|
|
101
|
+
if (lower.includes('water bucket') || lower.includes('bucket')) return 'water bucket';
|
|
102
|
+
if (lower.includes('hoe') || lower.includes('till tool')) return 'hoe';
|
|
103
|
+
if (lower.includes('seeds') || lower.includes('seed')) return 'seeds';
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseMissingFarmItems(text) {
|
|
109
|
+
const lower = String(stripAnsi(text || '')).toLowerCase();
|
|
110
|
+
const hasMissingHint = /(missing|need|needs|requires?|don['’]?t have|dont have|lack|you need)/i.test(lower)
|
|
111
|
+
|| lower.includes('need the following items')
|
|
112
|
+
|| lower.includes('need following items')
|
|
113
|
+
|| lower.includes('missing items');
|
|
114
|
+
|
|
115
|
+
if (!hasMissingHint) {
|
|
116
|
+
const single = parseMissingFarmItem(text);
|
|
117
|
+
return single ? [single] : [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const found = [];
|
|
121
|
+
|
|
122
|
+
for (const item of FARM_RESTOCK_ORDER) {
|
|
123
|
+
if (item === 'watering can' && (lower.includes('watering can') || lower.includes('water can'))) {
|
|
124
|
+
found.push('watering can');
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (item === 'water bucket' && lower.includes('water bucket')) {
|
|
128
|
+
found.push('water bucket');
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (item === 'seeds' && (lower.includes('seeds') || lower.includes('seed'))) {
|
|
132
|
+
found.push('seeds');
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (item === 'hoe' && lower.includes('hoe')) {
|
|
136
|
+
found.push('hoe');
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (found.length > 0) return Array.from(new Set(found));
|
|
142
|
+
|
|
143
|
+
const single = parseMissingFarmItem(text);
|
|
144
|
+
return single ? [single] : [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeSeedItemName(raw) {
|
|
148
|
+
const t = String(raw || '').replace(/[^a-zA-Z\s]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
149
|
+
if (!t) return 'seeds';
|
|
150
|
+
const lower = t.toLowerCase();
|
|
151
|
+
if (lower.endsWith('seed') || lower.endsWith('seeds')) return lower;
|
|
152
|
+
return `${lower} seeds`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseSeedStockFromText(text) {
|
|
156
|
+
const clean = String(stripAnsi(text || ''));
|
|
157
|
+
const patterns = [
|
|
158
|
+
/([a-z][a-z\s-]*seeds?)\s*[x×:]\s*(\d{1,5})/ig,
|
|
159
|
+
/([a-z][a-z\s-]*seeds?)\s*\((\d{1,5})\)/ig,
|
|
160
|
+
/\*{0,2}([a-z][a-z\s-]*seeds?)\*{0,2}\s*[⎯—─\-:]\s*`?(\d{1,5})`?/ig,
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
let best = null;
|
|
164
|
+
for (const re of patterns) {
|
|
165
|
+
let m;
|
|
166
|
+
while ((m = re.exec(clean)) !== null) {
|
|
167
|
+
const itemName = normalizeSeedItemName(m[1]);
|
|
168
|
+
const qty = parseInt(m[2], 10) || 0;
|
|
169
|
+
if (!best || qty > best.qty) best = { itemName, qty };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return best;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseSeedStockFromComponents(msg) {
|
|
176
|
+
const candidates = [];
|
|
177
|
+
for (const b of getAllButtons(msg)) {
|
|
178
|
+
const label = String(b?.label || '');
|
|
179
|
+
const m = label.match(/([a-z][a-z\s-]*seeds?)\s*\((\d{1,5})\)/i)
|
|
180
|
+
|| label.match(/([a-z][a-z\s-]*seeds?)\s*[x×:]\s*(\d{1,5})/i);
|
|
181
|
+
if (m) {
|
|
182
|
+
candidates.push({ itemName: normalizeSeedItemName(m[1]), qty: parseInt(m[2], 10) || 0 });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const menu of getAllSelectMenus(msg)) {
|
|
187
|
+
for (const o of (menu.options || [])) {
|
|
188
|
+
const label = String(o?.label || '');
|
|
189
|
+
const m = label.match(/([a-z][a-z\s-]*seeds?)\s*\((\d{1,5})\)/i)
|
|
190
|
+
|| label.match(/([a-z][a-z\s-]*seeds?)\s*[x×:]\s*(\d{1,5})/i);
|
|
191
|
+
if (m) {
|
|
192
|
+
candidates.push({ itemName: normalizeSeedItemName(m[1]), qty: parseInt(m[2], 10) || 0 });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (candidates.length === 0) return null;
|
|
198
|
+
return candidates.sort((a, b) => b.qty - a.qty)[0];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function analyzeFarmState({ msg, text }) {
|
|
202
|
+
const lower = String(stripAnsi(text || '')).toLowerCase();
|
|
203
|
+
const buttons = getAllButtons(msg).filter(b => !b.disabled);
|
|
204
|
+
|
|
205
|
+
const hasManage = buttons.some(b => hasLabelWord(b, 'manage'));
|
|
206
|
+
const hasActionMenu = buttons.some(b => hasAny(b, ['hoe', 'water', 'plant', 'harvest', 'fertiliz', 'reap', 'collect']));
|
|
207
|
+
const hasAll = buttons.some(b => hasAny(b, [' all', 'all ', ':all', 'hoe all', 'water all', 'plant all', 'harvest all', 'confirm all']));
|
|
208
|
+
const hasPickSlots = buttons.some(b => hasAny(b, ['pick slot', 'pick slots']));
|
|
209
|
+
const seedStock = parseSeedStockFromComponents(msg) || parseSeedStockFromText(text);
|
|
210
|
+
const missing = parseMissingFarmItem(text);
|
|
211
|
+
|
|
212
|
+
let stage = 'unknown';
|
|
213
|
+
if (missing) stage = 'blocked-missing-item';
|
|
214
|
+
else if (hasManage) stage = 'overview';
|
|
215
|
+
else if (seedStock) stage = 'seed-selection';
|
|
216
|
+
else if (hasActionMenu) stage = 'action-selection';
|
|
217
|
+
else if (hasAll || hasPickSlots) stage = 'slot-apply';
|
|
218
|
+
else if (/cooldown|farm again|already farmed/i.test(lower)) stage = 'cooldown';
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
stage,
|
|
222
|
+
missing,
|
|
223
|
+
seedStock,
|
|
224
|
+
hasManage,
|
|
225
|
+
hasActionMenu,
|
|
226
|
+
hasAll,
|
|
227
|
+
hasPickSlots,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function getDisabledAllButtonForAction(msg, actionName) {
|
|
232
|
+
const allButtons = getAllButtons(msg); // include disabled
|
|
233
|
+
const keyMap = {
|
|
234
|
+
hoe: ['hoe all'],
|
|
235
|
+
water: ['water all'],
|
|
236
|
+
plant: ['plant all'],
|
|
237
|
+
harvest: ['harvest all'],
|
|
238
|
+
fertilize: ['fertilize all', 'fertilise all'],
|
|
239
|
+
};
|
|
240
|
+
const words = keyMap[actionName] || [];
|
|
241
|
+
return allButtons.find(b => b.disabled && words.some(w => String(b.label || '').toLowerCase().includes(w))) || null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function isEphemeralFlags(flags) {
|
|
245
|
+
const f = typeof flags === 'number' ? flags : (flags?.bitfield ?? 0);
|
|
246
|
+
return (f & 64) !== 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function safeJson(value) {
|
|
250
|
+
const seen = new WeakSet();
|
|
251
|
+
return JSON.stringify(value, (k, v) => {
|
|
252
|
+
if (typeof v === 'bigint') return v.toString();
|
|
253
|
+
if (typeof v === 'function') return `[Function:${v.name || 'anon'}]`;
|
|
254
|
+
if (v && typeof v === 'object') {
|
|
255
|
+
if (seen.has(v)) return '[Circular]';
|
|
256
|
+
seen.add(v);
|
|
257
|
+
}
|
|
258
|
+
return v;
|
|
259
|
+
}, 2);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function attachmentList(msg) {
|
|
263
|
+
const a = msg?.attachments;
|
|
264
|
+
if (!a) return [];
|
|
265
|
+
if (Array.isArray(a)) return a;
|
|
266
|
+
if (typeof a.values === 'function') return Array.from(a.values());
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function rawPayloadSnapshot(payload) {
|
|
271
|
+
if (!payload) return null;
|
|
272
|
+
const embeds = (payload.embeds || []).map((e, i) => ({
|
|
273
|
+
index: i,
|
|
274
|
+
title: e?.title || null,
|
|
275
|
+
description: e?.description || null,
|
|
276
|
+
url: e?.url || null,
|
|
277
|
+
image: e?.image?.url || null,
|
|
278
|
+
thumbnail: e?.thumbnail?.url || null,
|
|
279
|
+
footer: e?.footer?.text || null,
|
|
280
|
+
fields: (e?.fields || []).map(f => ({ name: f?.name || null, value: f?.value || null })),
|
|
281
|
+
}));
|
|
282
|
+
const attachments = attachmentList(payload).map((x, i) => ({
|
|
283
|
+
index: i,
|
|
284
|
+
id: x?.id || null,
|
|
285
|
+
name: x?.name || null,
|
|
286
|
+
contentType: x?.contentType || x?.content_type || null,
|
|
287
|
+
size: x?.size || null,
|
|
288
|
+
url: x?.url || null,
|
|
289
|
+
proxyURL: x?.proxyURL || x?.proxy_url || null,
|
|
290
|
+
height: x?.height || null,
|
|
291
|
+
width: x?.width || null,
|
|
292
|
+
}));
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
id: payload?.id || null,
|
|
296
|
+
channelId: payload?.channel?.id || payload?.channelId || null,
|
|
297
|
+
author: payload?.author ? { id: payload.author.id, username: payload.author.username } : null,
|
|
298
|
+
createdTimestamp: payload?.createdTimestamp || null,
|
|
299
|
+
editedTimestamp: payload?.editedTimestamp || null,
|
|
300
|
+
flags: payload?.flags?.bitfield ?? payload?.flags ?? 0,
|
|
301
|
+
content: payload?.content || '',
|
|
302
|
+
embeds,
|
|
303
|
+
attachments,
|
|
304
|
+
components: payload?.components || null,
|
|
305
|
+
cv2Text: payload?._cv2text || null,
|
|
306
|
+
cv2Buttons: payload?._cv2buttons || null,
|
|
307
|
+
cv2Components: payload?._cv2 || null,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function logEphemeralLike(tag, payload) {
|
|
312
|
+
if (!payload) return;
|
|
313
|
+
const text = String(stripAnsi(getFullText(payload) || payload?.content || '')).replace(/\s+/g, ' ').trim();
|
|
314
|
+
const flags = payload?.flags?.bitfield ?? payload?.flags ?? 0;
|
|
315
|
+
const eph = isEphemeralFlags(flags);
|
|
316
|
+
const isInteresting = eph || /ephemeral|only you can see|broken|broke|cannot|can't|unable|missing/i.test(text);
|
|
317
|
+
const raw = safeJson(rawPayloadSnapshot(payload));
|
|
318
|
+
const line = `[farm:${tag}] interaction flags=${flags} ephemeral=${eph} text=${brief(text, 220) || '(empty)'} raw=${raw}`;
|
|
319
|
+
if (isInteresting) LOG.warn(line);
|
|
320
|
+
else LOG.info(line);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function analyzeFarmImageForAction(msg, actionName) {
|
|
324
|
+
try {
|
|
325
|
+
const url = extractFarmImageUrl(msg);
|
|
326
|
+
if (!url) {
|
|
327
|
+
const embedCount = Array.isArray(msg?.embeds) ? msg.embeds.length : 0;
|
|
328
|
+
const compCount = Array.isArray(msg?.components) ? msg.components.length : 0;
|
|
329
|
+
const attCount = (() => {
|
|
330
|
+
const a = msg?.attachments;
|
|
331
|
+
if (!a) return 0;
|
|
332
|
+
if (Array.isArray(a)) return a.length;
|
|
333
|
+
if (typeof a.size === 'number') return a.size;
|
|
334
|
+
if (typeof a.values === 'function') return Array.from(a.values()).length;
|
|
335
|
+
return 0;
|
|
336
|
+
})();
|
|
337
|
+
LOG.info(`[farm:vision-img] no image url for action=${actionName} embeds=${embedCount} comps=${compCount} attachments=${attCount}`);
|
|
338
|
+
return { shouldRepeat: false, reason: 'no-image' };
|
|
339
|
+
}
|
|
340
|
+
const buf = await downloadImage(url);
|
|
341
|
+
const analysis = await analyzeFarmGrid(buf);
|
|
342
|
+
const shouldRepeat = evaluateActionNeed(actionName, analysis);
|
|
343
|
+
const score = evaluateActionScores(actionName, analysis);
|
|
344
|
+
let dbg = null;
|
|
345
|
+
try {
|
|
346
|
+
dbg = await dumpFarmVisionDebug({
|
|
347
|
+
imgBuffer: buf,
|
|
348
|
+
analysis,
|
|
349
|
+
actionName,
|
|
350
|
+
sourceUrl: url,
|
|
351
|
+
});
|
|
352
|
+
} catch (e) {
|
|
353
|
+
LOG.warn(`[farm:vision-img] debug dump failed (${actionName}): ${e.message}`);
|
|
354
|
+
}
|
|
355
|
+
LOG.info(`[farm:vision-img] action=${actionName} counts=${JSON.stringify(analysis.counts)} conf=${analysis.avgConfidence} grid=${gridToString(analysis)} score=${score.score}/${score.threshold} matched=${score.matched} reason=${score.reason} repeat=${shouldRepeat}`);
|
|
356
|
+
if (dbg) {
|
|
357
|
+
const tileSummary = (analysis.cells || [])
|
|
358
|
+
.map(c => `r${c.row + 1}c${c.col + 1}:${c.state}:${c.confidence}`)
|
|
359
|
+
.join(' | ');
|
|
360
|
+
LOG.info(`[farm:vision-img] debug dir=${dbg.dir} source=${dbg.sourcePath} manifest=${dbg.manifestPath} tiles=${dbg.tileCount}`);
|
|
361
|
+
LOG.info(`[farm:vision-img] tile-summary ${tileSummary}`);
|
|
362
|
+
}
|
|
363
|
+
return { shouldRepeat, analysis, score, reason: 'ok' };
|
|
364
|
+
} catch (e) {
|
|
365
|
+
LOG.warn(`[farm:vision-img] analysis failed (${actionName}): ${e.message}`);
|
|
366
|
+
return { shouldRepeat: false, reason: 'error' };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function getVisionRepairPlan(actionName, score) {
|
|
371
|
+
if (!score || score.matched) return null;
|
|
372
|
+
|
|
373
|
+
if (actionName === 'water') {
|
|
374
|
+
return { itemName: 'watering can', quantity: 1, label: 'watering can' };
|
|
375
|
+
}
|
|
376
|
+
if (actionName === 'plant') {
|
|
377
|
+
const planted = score.counts?.planted || 0;
|
|
378
|
+
const slots = score.counts?.slots || FARM_TOTAL_SLOTS;
|
|
379
|
+
const deficit = Math.max(1, Math.min(FARM_TOTAL_SLOTS, slots - planted));
|
|
380
|
+
return { itemName: 'seeds', quantity: deficit, label: 'seeds' };
|
|
381
|
+
}
|
|
382
|
+
if (actionName === 'hoe') {
|
|
383
|
+
const planted = score.ratios?.planted || 0;
|
|
384
|
+
const wet = score.ratios?.wet || 0;
|
|
385
|
+
if ((planted + wet) >= 0.45) {
|
|
386
|
+
// Farm likely already progressed beyond hoe stage.
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
return { itemName: 'hoe', quantity: 1, label: 'hoe' };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function inferNextActionFromScore(actionName, score) {
|
|
396
|
+
if (!score) return null;
|
|
397
|
+
const planted = score.ratios?.planted || 0;
|
|
398
|
+
const wet = score.ratios?.wet || 0;
|
|
399
|
+
const tilled = score.ratios?.tilled || 0;
|
|
400
|
+
|
|
401
|
+
if (actionName === 'hoe' && (planted + wet) >= 0.45) {
|
|
402
|
+
return 'water';
|
|
403
|
+
}
|
|
404
|
+
if (actionName === 'water' && planted >= 0.45) {
|
|
405
|
+
return 'plant';
|
|
406
|
+
}
|
|
407
|
+
if (actionName === 'harvest') {
|
|
408
|
+
// After harvest completes, always restart lifecycle from hoe.
|
|
409
|
+
if (tilled >= 0.45 || planted <= 0.35) return 'hoe';
|
|
410
|
+
// Still mostly planted -> keep harvest context; do not jump to plant.
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function inferPreferredActionFromImage(msg) {
|
|
417
|
+
try {
|
|
418
|
+
const url = extractFarmImageUrl(msg);
|
|
419
|
+
if (!url) return null;
|
|
420
|
+
const buf = await downloadImage(url);
|
|
421
|
+
const analysis = await analyzeFarmGrid(buf);
|
|
422
|
+
const slots = Math.max(1, (analysis?.rows || 3) * (analysis?.cols || 3));
|
|
423
|
+
const ratios = {
|
|
424
|
+
tilled: (analysis?.counts?.tilled || 0) / slots,
|
|
425
|
+
wet: (analysis?.counts?.wet || 0) / slots,
|
|
426
|
+
planted: (analysis?.counts?.planted || 0) / slots,
|
|
427
|
+
unknown: (analysis?.counts?.unknown || 0) / slots,
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
let suggested = null;
|
|
431
|
+
// Image-first phase routing.
|
|
432
|
+
if (ratios.planted >= 0.45) suggested = 'plant';
|
|
433
|
+
else if (ratios.wet >= 0.35) suggested = 'plant';
|
|
434
|
+
else if (ratios.tilled >= 0.35) suggested = 'water';
|
|
435
|
+
else suggested = 'hoe';
|
|
436
|
+
|
|
437
|
+
let dbg = null;
|
|
438
|
+
try {
|
|
439
|
+
dbg = await dumpFarmVisionDebug({
|
|
440
|
+
imgBuffer: buf,
|
|
441
|
+
analysis,
|
|
442
|
+
actionName: `phase-${suggested || 'unknown'}`,
|
|
443
|
+
sourceUrl: url,
|
|
444
|
+
});
|
|
445
|
+
} catch (e) {
|
|
446
|
+
LOG.warn(`[farm:phase-image] debug dump failed: ${e.message}`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
LOG.info(`[farm:phase-image] url=${url} grid=${gridToString(analysis)} counts=${JSON.stringify(analysis.counts)} conf=${analysis.avgConfidence} suggested=${suggested} ratios=${JSON.stringify({
|
|
450
|
+
tilled: +ratios.tilled.toFixed(3),
|
|
451
|
+
wet: +ratios.wet.toFixed(3),
|
|
452
|
+
planted: +ratios.planted.toFixed(3),
|
|
453
|
+
unknown: +ratios.unknown.toFixed(3),
|
|
454
|
+
})}`);
|
|
455
|
+
if (dbg) {
|
|
456
|
+
const tileSummary = (analysis.cells || [])
|
|
457
|
+
.map(c => `r${c.row + 1}c${c.col + 1}:${c.state}:${c.confidence}`)
|
|
458
|
+
.join(' | ');
|
|
459
|
+
LOG.info(`[farm:phase-image] debug dir=${dbg.dir} source=${dbg.sourcePath} manifest=${dbg.manifestPath} tiles=${dbg.tileCount}`);
|
|
460
|
+
LOG.info(`[farm:phase-image] tile-summary ${tileSummary}`);
|
|
461
|
+
}
|
|
462
|
+
return suggested;
|
|
463
|
+
} catch (e) {
|
|
464
|
+
LOG.warn(`[farm:phase-image] inference failed: ${e.message}`);
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function chooseSeedOption(menu) {
|
|
470
|
+
const options = (menu?.options || []).filter(o => o && o.value);
|
|
471
|
+
if (options.length === 0) return null;
|
|
472
|
+
|
|
473
|
+
const withQty = options.map(o => {
|
|
474
|
+
const label = String(o.label || '');
|
|
475
|
+
const m = label.match(/\((\d{1,5})\)/);
|
|
476
|
+
return { option: o, qty: m ? parseInt(m[1], 10) : 0, isDefault: !!o.default };
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const defaultOpt = withQty.find(x => x.isDefault && x.qty > 0);
|
|
480
|
+
if (defaultOpt) return defaultOpt.option;
|
|
481
|
+
|
|
482
|
+
const best = withQty.sort((a, b) => b.qty - a.qty)[0];
|
|
483
|
+
return best?.option || options[0];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function extractSelectMenusFromCv2Tree(nodes, out = []) {
|
|
487
|
+
for (const n of nodes || []) {
|
|
488
|
+
if (!n || typeof n !== 'object') continue;
|
|
489
|
+
const t = n.type;
|
|
490
|
+
if (t === 3 || t === 'SELECT_MENU' || t === 'STRING_SELECT') {
|
|
491
|
+
out.push(n);
|
|
492
|
+
}
|
|
493
|
+
if (Array.isArray(n.components) && n.components.length > 0) {
|
|
494
|
+
extractSelectMenusFromCv2Tree(n.components, out);
|
|
495
|
+
}
|
|
496
|
+
if (Array.isArray(n.items) && n.items.length > 0) {
|
|
497
|
+
extractSelectMenusFromCv2Tree(n.items, out);
|
|
498
|
+
}
|
|
499
|
+
if (Array.isArray(n.data?.items) && n.data.items.length > 0) {
|
|
500
|
+
extractSelectMenusFromCv2Tree(n.data.items, out);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function ensurePlantSeedSelected({ response, waitForDankMemer, channel }) {
|
|
507
|
+
const menus = [
|
|
508
|
+
...getAllSelectMenus(response),
|
|
509
|
+
...extractSelectMenusFromCv2Tree(response?._cv2),
|
|
510
|
+
];
|
|
511
|
+
const plantMenu = menus.find(m => (m.options || []).some(o => /seed|beans|potato|corn|carrot|wheat|pumpkin|plant/i.test(String(o?.label || ''))));
|
|
512
|
+
if (!plantMenu) return { response, selected: false, reason: 'no-plant-menu' };
|
|
513
|
+
|
|
514
|
+
const pick = chooseSeedOption(plantMenu);
|
|
515
|
+
if (!pick?.value) return { response, selected: false, reason: 'no-seed-option' };
|
|
516
|
+
|
|
517
|
+
LOG.info(`[farm:plant] selecting seed option label="${pick.label || '-'}" value="${pick.value}" default=${!!pick.default}`);
|
|
518
|
+
try {
|
|
519
|
+
const menuId = plantMenu.customId || plantMenu.custom_id || 0;
|
|
520
|
+
try {
|
|
521
|
+
await response.selectMenu(menuId, [pick.value]);
|
|
522
|
+
} catch (e) {
|
|
523
|
+
LOG.warn(`[farm:plant] selectMenu() failed, trying CV2 select fallback: ${e.message}`);
|
|
524
|
+
await clickCV2SelectMenu(response, menuId, [pick.value]);
|
|
525
|
+
}
|
|
526
|
+
let updated = await waitForDankMemer(7000);
|
|
527
|
+
if (!updated && response.id) {
|
|
528
|
+
const baseline = brief(getFullText(response), 300);
|
|
529
|
+
updated = await waitForEditedMessage(channel, response.id, baseline, 7000);
|
|
530
|
+
}
|
|
531
|
+
if (updated) {
|
|
532
|
+
if (isCV2(updated)) await ensureCV2(updated);
|
|
533
|
+
logMsg(updated, 'farm-plant-seed-selected');
|
|
534
|
+
logFarmState('plant-seed-selected', updated);
|
|
535
|
+
logFarmDeepState('plant-seed-selected', updated);
|
|
536
|
+
return { response: updated, selected: true, reason: 'selected' };
|
|
537
|
+
}
|
|
538
|
+
return { response, selected: true, reason: 'selected-no-update' };
|
|
539
|
+
} catch (e) {
|
|
540
|
+
LOG.warn(`[farm:plant] seed selection failed: ${e.message}`);
|
|
541
|
+
return { response, selected: false, reason: 'select-failed' };
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function capturePhaseVisionScore({ msg, actionName, phaseTag }) {
|
|
546
|
+
if (!msg || !['hoe', 'water', 'plant', 'harvest'].includes(actionName)) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const check = await analyzeFarmImageForAction(msg, actionName);
|
|
551
|
+
const s = check?.score;
|
|
552
|
+
if (s) {
|
|
553
|
+
LOG.info(`[farm:phase-score] phase=${phaseTag} action=${actionName} score=${s.score}/${s.threshold} matched=${s.matched} conf=${s.confidence} reason=${s.reason}`);
|
|
554
|
+
} else {
|
|
555
|
+
LOG.info(`[farm:phase-score] phase=${phaseTag} action=${actionName} score=unavailable reason=${check?.reason || 'unknown'}`);
|
|
556
|
+
}
|
|
557
|
+
return check;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function mergeBuyPlan(plan) {
|
|
561
|
+
const m = new Map();
|
|
562
|
+
for (const step of plan || []) {
|
|
563
|
+
if (!step?.item) continue;
|
|
564
|
+
const key = String(step.item).toLowerCase();
|
|
565
|
+
const qty = Math.max(1, Number(step.qty || 1));
|
|
566
|
+
m.set(key, Math.max(qty, m.get(key) || 0));
|
|
567
|
+
}
|
|
568
|
+
return Array.from(m.entries()).map(([item, qty]) => ({ item, qty }));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function extractActionQtyFromPage({ action, msg, text }) {
|
|
572
|
+
const lower = String(stripAnsi(text || '')).toLowerCase();
|
|
573
|
+
const labels = [
|
|
574
|
+
...getAllButtons(msg).map(b => String(b.label || '')),
|
|
575
|
+
...getAllSelectMenus(msg).flatMap(m => (m.options || []).map(o => String(o.label || ''))),
|
|
576
|
+
String(stripAnsi(text || '')),
|
|
577
|
+
];
|
|
578
|
+
|
|
579
|
+
const keyWords = {
|
|
580
|
+
hoe: ['hoe'],
|
|
581
|
+
water: ['water can', 'watering can', 'water bucket', 'bucket'],
|
|
582
|
+
plant: ['seed', 'seeds'],
|
|
583
|
+
};
|
|
584
|
+
const keys = keyWords[action] || [];
|
|
585
|
+
|
|
586
|
+
let bestQty = null;
|
|
587
|
+
let sawKeyword = false;
|
|
588
|
+
|
|
589
|
+
for (const src of labels) {
|
|
590
|
+
const s = String(src || '');
|
|
591
|
+
const sl = s.toLowerCase();
|
|
592
|
+
if (keys.some(k => sl.includes(k))) sawKeyword = true;
|
|
593
|
+
|
|
594
|
+
// patterns: Item (9), Item x9, Item: 9
|
|
595
|
+
for (const k of keys) {
|
|
596
|
+
const reList = [
|
|
597
|
+
new RegExp(`${k}[^\\d]{0,12}\\((\\d{1,5})\\)`, 'i'),
|
|
598
|
+
new RegExp(`${k}[^\\d]{0,12}[x×:]\\s*(\\d{1,5})`, 'i'),
|
|
599
|
+
];
|
|
600
|
+
for (const re of reList) {
|
|
601
|
+
const m = s.match(re);
|
|
602
|
+
if (m) {
|
|
603
|
+
const q = parseInt(m[1], 10);
|
|
604
|
+
if (Number.isFinite(q) && (bestQty == null || q > bestQty)) bestQty = q;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return { sawKeyword, qty: bestQty };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function tryBuyFarmItem({ missing, channel, waitForDankMemer, client }) {
|
|
614
|
+
return tryBuyFarmItemQty({ missing, quantity: 1, channel, waitForDankMemer, client });
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function tryBuyFarmItemQty({ missing, quantity = 1, channel, waitForDankMemer, client }) {
|
|
618
|
+
const aliases = FARM_BUY_ALIASES[missing] || [missing];
|
|
619
|
+
for (const itemName of aliases) {
|
|
620
|
+
const ok = await buyItem({ channel, waitForDankMemer, client, itemName, quantity });
|
|
621
|
+
if (ok) return { ok: true, itemName };
|
|
622
|
+
}
|
|
623
|
+
return { ok: false, itemName: aliases[0] || missing };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function brief(text, n = 140) {
|
|
627
|
+
return String(stripAnsi(text || '')).replace(/\s+/g, ' ').trim().slice(0, n);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function logFarmState(tag, msg) {
|
|
631
|
+
const text = brief(getFullText(msg), 180);
|
|
632
|
+
const buttons = getAllButtons(msg)
|
|
633
|
+
.map(b => `${b.disabled ? 'off' : 'on'}:${b.label || '-'}:${b.customId || b.custom_id || '-'}`)
|
|
634
|
+
.slice(0, 12);
|
|
635
|
+
const menus = getAllSelectMenus(msg);
|
|
636
|
+
LOG.info(`[farm:${tag}] text=${text || '(empty)'}`);
|
|
637
|
+
if (buttons.length > 0) LOG.info(`[farm:${tag}] buttons=${buttons.join(' | ')}`);
|
|
638
|
+
if (menus.length > 0) LOG.info(`[farm:${tag}] select_menus=${menus.length}`);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function logFarmDeepState(tag, msg) {
|
|
642
|
+
const buttons = getAllButtons(msg);
|
|
643
|
+
const menus = getAllSelectMenus(msg);
|
|
644
|
+
LOG.info(`[farm:${tag}] deep buttons=${buttons.length} menus=${menus.length}`);
|
|
645
|
+
|
|
646
|
+
if (buttons.length > 0) {
|
|
647
|
+
const lines = buttons.map((b, i) => {
|
|
648
|
+
const qty = parseButtonQty(b);
|
|
649
|
+
return `${i + 1}. ${b.disabled ? 'DIS' : 'EN'} label="${b.label || '-'}" qty=${qty ?? '-'} id="${b.customId || b.custom_id || '-'}"`;
|
|
650
|
+
});
|
|
651
|
+
LOG.info(`[farm:${tag}] deep button_list:\n${lines.join('\n')}`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (menus.length > 0) {
|
|
655
|
+
for (let i = 0; i < menus.length; i++) {
|
|
656
|
+
const m = menus[i];
|
|
657
|
+
const opts = (m.options || []).map((o, oi) => `${oi + 1}) ${o.label || '-'} [${o.value || '-'}]${o.default ? ' *default*' : ''}`);
|
|
658
|
+
LOG.info(`[farm:${tag}] deep menu#${i + 1} id="${m.customId || m.custom_id || '-'}" options=${(m.options || []).length}`);
|
|
659
|
+
if (opts.length > 0) LOG.info(`[farm:${tag}] deep menu#${i + 1} options: ${opts.join(' | ')}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function isFarmActionButton(btn) {
|
|
665
|
+
const label = String(btn?.label || '').toLowerCase();
|
|
666
|
+
const id = String(btn?.customId || btn?.custom_id || '').toLowerCase();
|
|
667
|
+
const hay = `${label} ${id}`;
|
|
668
|
+
return [
|
|
669
|
+
'harvest', 'water', 'plant', 'collect', 'claim', 'reap', 'fertiliz', 'spray', 'seed', 'crop', 'confirm',
|
|
670
|
+
].some(k => hay.includes(k));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function buttonHay(btn) {
|
|
674
|
+
return `${String(btn?.label || '').toLowerCase()} ${String(btn?.customId || btn?.custom_id || '').toLowerCase()}`;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function hasAny(btn, words) {
|
|
678
|
+
const hay = buttonHay(btn);
|
|
679
|
+
return words.some(w => hay.includes(w));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function parseButtonQty(btn) {
|
|
683
|
+
const label = String(btn?.label || '');
|
|
684
|
+
const m = label.match(/\((\d+)\)/);
|
|
685
|
+
return m ? parseInt(m[1], 10) : null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function isNavOrUtilityButton(btn) {
|
|
689
|
+
const label = String(btn?.label || '').trim().toLowerCase();
|
|
690
|
+
const id = String(btn?.customId || btn?.custom_id || '').toLowerCase();
|
|
691
|
+
if (!label || label === '-' || label === 'back' || label === 'go back') return true;
|
|
692
|
+
if (id.includes('goback') || id.includes('selectfarm') || id.includes('changeSkin'.toLowerCase()) || id.includes('rename') || id.includes('buyplot') || id.includes('farm-farm')) return true;
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function hasLabelWord(btn, word) {
|
|
697
|
+
const label = String(btn?.label || '').toLowerCase();
|
|
698
|
+
return new RegExp(`\\b${word}\\b`, 'i').test(label);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function getManageActionButtons(msg) {
|
|
702
|
+
const btns = getAllButtons(msg).filter(b => !b.disabled && !isNavOrUtilityButton(b));
|
|
703
|
+
const pickByLabel = (words) => btns.find(b => words.some(w => hasLabelWord(b, w))) || null;
|
|
704
|
+
const pickFallback = (words) => btns.find(b => hasAny(b, words)) || null;
|
|
705
|
+
const pick = (words) => pickByLabel(words) || pickFallback(words);
|
|
706
|
+
return {
|
|
707
|
+
hoe: pick(['hoe', 'till']),
|
|
708
|
+
water: pick(['water', 'watering']),
|
|
709
|
+
plant: pick(['plant', 'seed', 'sow']),
|
|
710
|
+
fertilize: pick(['fertiliz', 'compost', 'manure']),
|
|
711
|
+
harvest: pick(['harvest', 'reap', 'collect']),
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function restockFromManageCounts({ response, channel, waitForDankMemer, client }) {
|
|
716
|
+
const actions = getManageActionButtons(response);
|
|
717
|
+
const counts = {
|
|
718
|
+
hoe: parseButtonQty(actions.hoe),
|
|
719
|
+
water: parseButtonQty(actions.water),
|
|
720
|
+
plant: parseButtonQty(actions.plant),
|
|
721
|
+
fertilize: parseButtonQty(actions.fertilize),
|
|
722
|
+
harvest: parseButtonQty(actions.harvest),
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
LOG.info(`[farm] manage counts hoe=${counts.hoe ?? '-'} water=${counts.water ?? '-'} plant=${counts.plant ?? '-'} fert=${counts.fertilize ?? '-'} harvest=${counts.harvest ?? '-'}`);
|
|
726
|
+
LOG.info(`[farm] manage labels hoe="${actions.hoe?.label || '-'}" water="${actions.water?.label || '-'}" plant="${actions.plant?.label || '-'}" fert="${actions.fertilize?.label || '-'}" harvest="${actions.harvest?.label || '-'}"`);
|
|
727
|
+
|
|
728
|
+
const buyPlan = [];
|
|
729
|
+
if (counts.hoe === 0) buyPlan.push({ item: 'hoe', qty: 1 });
|
|
730
|
+
if (counts.water === 0) buyPlan.push({ item: 'watering can', qty: 1 });
|
|
731
|
+
|
|
732
|
+
// Plant count behaves like available seed quantity/options in many CV2 states.
|
|
733
|
+
if (counts.plant === 0) buyPlan.push({ item: 'seeds', qty: FARM_TOTAL_SLOTS });
|
|
734
|
+
else if (Number.isFinite(counts.plant) && counts.plant > 0 && counts.plant < FARM_TOTAL_SLOTS) {
|
|
735
|
+
buyPlan.push({ item: 'seeds', qty: FARM_TOTAL_SLOTS - counts.plant });
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Fertilizer is optional: use if present, never buy.
|
|
739
|
+
|
|
740
|
+
if (buyPlan.length === 0) return { boughtAny: false };
|
|
741
|
+
|
|
742
|
+
LOG.info(`[farm] restock plan: ${buyPlan.map(p => `${p.item} x${p.qty}`).join(', ')}`);
|
|
743
|
+
|
|
744
|
+
for (const step of buyPlan) {
|
|
745
|
+
LOG.warn(`[farm] Restock needed: ${step.item} x${step.qty}`);
|
|
746
|
+
const ok = await buyItem({ channel, waitForDankMemer, client, itemName: step.item, quantity: step.qty });
|
|
747
|
+
if (!ok) {
|
|
748
|
+
LOG.warn(`[farm] Could not buy ${step.item} x${step.qty} — retrying in 1h`);
|
|
749
|
+
return {
|
|
750
|
+
boughtAny: false,
|
|
751
|
+
failed: true,
|
|
752
|
+
result: `need ${step.item} x${step.qty} (buy failed)`,
|
|
753
|
+
coins: 0,
|
|
754
|
+
nextCooldownSec: 3600,
|
|
755
|
+
skipReason: 'farm_restock_failed',
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
await sleep(600);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return { boughtAny: true };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function findFarmButton(msg, words, { includeDisabled = false } = {}) {
|
|
765
|
+
const btns = getAllButtons(msg).filter(b => includeDisabled || !b.disabled);
|
|
766
|
+
return btns.find(b => hasAny(b, words)) || null;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function pickFarmActionButton(msg, text) {
|
|
770
|
+
const btns = getAllButtons(msg).filter(b => !b.disabled && !isNavOrUtilityButton(b));
|
|
771
|
+
const allBtns = getAllButtons(msg);
|
|
772
|
+
const lower = String(stripAnsi(text || '')).toLowerCase();
|
|
773
|
+
|
|
774
|
+
const hasEnabledAll = (word) => allBtns.some(b => !b.disabled && String(b.label || '').toLowerCase().includes(word));
|
|
775
|
+
const hasDisabledAll = (word) => allBtns.some(b => b.disabled && String(b.label || '').toLowerCase().includes(word));
|
|
776
|
+
|
|
777
|
+
const byLabel = (word) => btns.find(b => hasLabelWord(b, word)) || null;
|
|
778
|
+
|
|
779
|
+
// Explicit empty-farm heuristic: always start with Hoe when possible.
|
|
780
|
+
if (/seems pretty empty|pretty empty|empty\.\.\./i.test(lower)) {
|
|
781
|
+
const hoeFirst = byLabel('hoe') || btns.find(b => hasAny(b, ['hoe', 'till', 'cleanup', 'clean']));
|
|
782
|
+
if (hoeFirst) return { action: 'hoe', button: hoeFirst };
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Progression-aware transitions for farm lifecycle.
|
|
786
|
+
// If current phase appears completed (its "All" got disabled), move forward.
|
|
787
|
+
if (hasDisabledAll('hoe all')) {
|
|
788
|
+
if (hasEnabledAll('water all') || byLabel('water')) {
|
|
789
|
+
const water = byLabel('water') || btns.find(b => hasAny(b, ['water', 'watering']));
|
|
790
|
+
if (water) return { action: 'water', button: water };
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (hasDisabledAll('water all')) {
|
|
794
|
+
if (hasEnabledAll('plant all') || byLabel('plant')) {
|
|
795
|
+
const plant = byLabel('plant') || btns.find(b => hasAny(b, ['plant', 'seed', 'sow']));
|
|
796
|
+
if (plant) return { action: 'plant', button: plant };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (hasDisabledAll('plant all')) {
|
|
800
|
+
if (hasEnabledAll('fertilize all')) {
|
|
801
|
+
const fert = byLabel('fertilize') || btns.find(b => hasAny(b, ['fertiliz', 'compost', 'manure']));
|
|
802
|
+
if (fert) return { action: 'fertilize', button: fert };
|
|
803
|
+
}
|
|
804
|
+
// Only fast-forward to harvest when text indicates harvest-like state.
|
|
805
|
+
const harvestHint = /ready|harvest|ripe|wilt|dead plant|collect/i.test(lower);
|
|
806
|
+
if (harvestHint && (hasEnabledAll('harvest all') || byLabel('harvest'))) {
|
|
807
|
+
const harvest = byLabel('harvest') || btns.find(b => hasAny(b, ['harvest', 'reap', 'collect']));
|
|
808
|
+
if (harvest) return { action: 'harvest', button: harvest };
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const buckets = [
|
|
813
|
+
{ key: 'harvest', words: ['harvest', 'reap', 'collect'] },
|
|
814
|
+
{ key: 'hoe', words: ['hoe', 'till', 'cleanup', 'clean'] },
|
|
815
|
+
{ key: 'water', words: ['water', 'watering'] },
|
|
816
|
+
{ key: 'plant', words: ['plant', 'seed', 'sow'] },
|
|
817
|
+
{ key: 'fertilize', words: ['fertiliz', 'manure', 'compost'] },
|
|
818
|
+
];
|
|
819
|
+
|
|
820
|
+
// Context-sensitive priority from farm status text.
|
|
821
|
+
const priority = [];
|
|
822
|
+
if (/harvest|wilt|dead plant|ready/i.test(lower)) priority.push('harvest');
|
|
823
|
+
if (/mess|debris|cleanup|empty|untilled|till/i.test(lower)) priority.push('hoe');
|
|
824
|
+
if (/dry|water/i.test(lower)) priority.push('water');
|
|
825
|
+
if (/seed|plant/i.test(lower)) priority.push('plant');
|
|
826
|
+
priority.push('harvest', 'hoe', 'water', 'plant', 'fertilize');
|
|
827
|
+
|
|
828
|
+
const seen = new Set();
|
|
829
|
+
const ordered = priority.filter(p => (seen.has(p) ? false : (seen.add(p), true)));
|
|
830
|
+
|
|
831
|
+
for (const p of ordered) {
|
|
832
|
+
const bucket = buckets.find(b => b.key === p);
|
|
833
|
+
if (!bucket) continue;
|
|
834
|
+
const hit = btns.find(b => bucket.words.some(w => hasLabelWord(b, w)))
|
|
835
|
+
|| btns.find(b => hasAny(b, bucket.words));
|
|
836
|
+
if (hit) return { action: p, button: hit };
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async function clickAndCapture({ channel, waitForDankMemer, response, button, tag, timeoutMs = 9000 }) {
|
|
843
|
+
const baseline = brief(getFullText(response), 400);
|
|
844
|
+
const clickRes = await safeClickButton(response, button);
|
|
845
|
+
logEphemeralLike(`${tag}-clickRes`, clickRes);
|
|
846
|
+
if (response?._lastInteractionAck) {
|
|
847
|
+
logEphemeralLike(`${tag}-interactionAck`, response._lastInteractionAck);
|
|
848
|
+
delete response._lastInteractionAck;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
let post = clickRes || null;
|
|
852
|
+
if (!post && response.id) post = await waitForEditedMessage(channel, response.id, baseline, timeoutMs);
|
|
853
|
+
if (!post) post = await waitForDankMemer(timeoutMs);
|
|
854
|
+
|
|
855
|
+
if (!post) return null;
|
|
856
|
+
logEphemeralLike(`${tag}-post`, post);
|
|
857
|
+
|
|
858
|
+
// Capture any additional immediate callback message (often ephemeral-like
|
|
859
|
+
// "only you can see this" notices) that may be separate from edited CV2 post.
|
|
860
|
+
try {
|
|
861
|
+
const side = await waitForDankMemer(1500);
|
|
862
|
+
if (side && side.id !== post.id) {
|
|
863
|
+
logEphemeralLike(`${tag}-post-extra`, side);
|
|
864
|
+
post._farmExtraInteraction = side;
|
|
865
|
+
}
|
|
866
|
+
} catch {}
|
|
867
|
+
|
|
868
|
+
if (isCV2(post)) await ensureCV2(post);
|
|
869
|
+
logMsg(post, tag);
|
|
870
|
+
logFarmState(tag, post);
|
|
871
|
+
return post;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async function auditManageAndBuildBuyPlan({ response, channel, waitForDankMemer, client }) {
|
|
875
|
+
const rawPlan = [];
|
|
876
|
+
let current = response;
|
|
877
|
+
const auditSeq = [
|
|
878
|
+
{ action: 'hoe', requiredItem: 'hoe' },
|
|
879
|
+
{ action: 'water', requiredItem: 'watering can' },
|
|
880
|
+
{ action: 'plant', requiredItem: 'seeds' },
|
|
881
|
+
];
|
|
882
|
+
|
|
883
|
+
for (const step of auditSeq) {
|
|
884
|
+
const actions = getManageActionButtons(current);
|
|
885
|
+
const actionBtn = actions[step.action];
|
|
886
|
+
if (!actionBtn) continue;
|
|
887
|
+
|
|
888
|
+
// Move to the action tab to inspect *All state and any hidden warnings.
|
|
889
|
+
try {
|
|
890
|
+
const moved = await clickAndCapture({
|
|
891
|
+
channel,
|
|
892
|
+
waitForDankMemer,
|
|
893
|
+
response: current,
|
|
894
|
+
button: actionBtn,
|
|
895
|
+
tag: `farm-audit-${step.action}`,
|
|
896
|
+
timeoutMs: 8000,
|
|
897
|
+
});
|
|
898
|
+
if (moved) current = moved;
|
|
899
|
+
} catch (e) {
|
|
900
|
+
LOG.warn(`[farm] audit ${step.action} click failed: ${e.message}`);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const currentText = getFullText(current) || '';
|
|
904
|
+
const lower = String(stripAnsi(currentText)).toLowerCase();
|
|
905
|
+
const disabledAll = getDisabledAllButtonForAction(current, step.action);
|
|
906
|
+
const missingItems = parseMissingFarmItems(lower);
|
|
907
|
+
const actionQty = extractActionQtyFromPage({ action: step.action, msg: current, text: currentText });
|
|
908
|
+
LOG.info(`[farm:audit-${step.action}] qty=${actionQty.qty ?? '-'} sawKeyword=${actionQty.sawKeyword} disabledAll=${!!disabledAll}`);
|
|
909
|
+
|
|
910
|
+
if (step.action === 'plant') {
|
|
911
|
+
const seedInfo = analyzeFarmState({ msg: current, text: getFullText(current) }).seedStock;
|
|
912
|
+
if (seedInfo && Number.isFinite(seedInfo.qty)) {
|
|
913
|
+
const deficit = Math.max(0, FARM_TOTAL_SLOTS - seedInfo.qty);
|
|
914
|
+
if (deficit > 0) rawPlan.push({ item: seedInfo.itemName || 'seeds', qty: deficit });
|
|
915
|
+
} else if (Number.isFinite(actionQty.qty)) {
|
|
916
|
+
const deficit = Math.max(0, FARM_TOTAL_SLOTS - actionQty.qty);
|
|
917
|
+
if (deficit > 0) rawPlan.push({ item: 'seeds', qty: deficit });
|
|
918
|
+
}
|
|
919
|
+
} else {
|
|
920
|
+
if (Number.isFinite(actionQty.qty) && actionQty.qty <= 0) {
|
|
921
|
+
rawPlan.push({ item: step.requiredItem, qty: 1 });
|
|
922
|
+
} else if (disabledAll) {
|
|
923
|
+
rawPlan.push({ item: step.requiredItem, qty: 1 });
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
for (const mi of missingItems) {
|
|
928
|
+
if (mi === 'seeds') rawPlan.push({ item: 'seeds', qty: FARM_TOTAL_SLOTS });
|
|
929
|
+
else rawPlan.push({ item: mi, qty: 1 });
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Additional explicit seed-missing detection from current action text.
|
|
933
|
+
const actionText = String(stripAnsi(getFullText(current) || '')).toLowerCase();
|
|
934
|
+
if (step.action === 'plant') {
|
|
935
|
+
if (/no\s+seeds|need\s+seeds|missing\s+seeds|don['’]?t\s+have\s+.*seed/i.test(actionText)) {
|
|
936
|
+
rawPlan.push({ item: 'seeds', qty: FARM_TOTAL_SLOTS });
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const buyPlan = mergeBuyPlan(rawPlan)
|
|
942
|
+
.filter(p => p.item !== 'fertilize' && p.item !== 'fertilizer')
|
|
943
|
+
.sort((a, b) => FARM_RESTOCK_ORDER.indexOf(a.item) - FARM_RESTOCK_ORDER.indexOf(b.item));
|
|
944
|
+
|
|
945
|
+
return { buyPlan, response: current };
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function isFarmManagementButton(btn) {
|
|
949
|
+
const label = String(btn?.label || '').toLowerCase();
|
|
950
|
+
const id = String(btn?.customId || btn?.custom_id || '').toLowerCase();
|
|
951
|
+
const hay = `${label} ${id}`;
|
|
952
|
+
return [
|
|
953
|
+
'manage', 'rename', 'change skin', 'buy plot', 'upgrade', 'settings', 'refresh', 'selectfarm', 'farm-farm',
|
|
954
|
+
].some(k => hay.includes(k));
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async function waitForEditedMessage(channel, messageId, baselineText, timeoutMs = 9000) {
|
|
958
|
+
const start = Date.now();
|
|
959
|
+
while (Date.now() - start < timeoutMs) {
|
|
960
|
+
await sleep(500);
|
|
961
|
+
try {
|
|
962
|
+
const fresh = await channel.messages.fetch(messageId);
|
|
963
|
+
if (!fresh) continue;
|
|
964
|
+
if (isCV2(fresh)) await ensureCV2(fresh, true);
|
|
965
|
+
const next = brief(getFullText(fresh), 400);
|
|
966
|
+
if (next && next !== baselineText) return fresh;
|
|
967
|
+
} catch {}
|
|
968
|
+
}
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async function runFarm({ channel, waitForDankMemer, client, _buyRetryDepth = 0, _visionRetryDepth = 0, _preferredAction = null }) {
|
|
973
|
+
LOG.cmd(`${c.white}${c.bold}pls farm view${c.reset}`);
|
|
974
|
+
|
|
975
|
+
await channel.send('pls farm view');
|
|
976
|
+
let response = await waitForDankMemer(12000);
|
|
977
|
+
|
|
978
|
+
if (!response) {
|
|
979
|
+
LOG.warn('[farm] No response');
|
|
980
|
+
return { result: 'no response', coins: 0 };
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (isHoldTight(response)) {
|
|
984
|
+
const reason = getHoldTightReason(response);
|
|
985
|
+
LOG.warn(`[farm] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
|
|
986
|
+
await sleep(30000);
|
|
987
|
+
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (isCV2(response)) await ensureCV2(response);
|
|
991
|
+
logMsg(response, 'farm');
|
|
992
|
+
logFarmState('initial', response);
|
|
993
|
+
logFarmDeepState('initial', response);
|
|
994
|
+
|
|
995
|
+
let text = getFullText(response);
|
|
996
|
+
let clean = brief(text, 600);
|
|
997
|
+
const lower = clean.toLowerCase();
|
|
998
|
+
let farmVision = analyzeFarmState({ msg: response, text });
|
|
999
|
+
LOG.info(`[farm:vision] stage=${farmVision.stage}${farmVision.seedStock ? ` seed=${farmVision.seedStock.itemName}:${farmVision.seedStock.qty}` : ''}${farmVision.missing ? ` missing=${farmVision.missing}` : ''}`);
|
|
1000
|
+
|
|
1001
|
+
if (farmVision.missing) {
|
|
1002
|
+
const earlyMissingItems = [farmVision.missing];
|
|
1003
|
+
LOG.warn(`[farm] Missing items: ${c.bold}${earlyMissingItems.join(', ')}${c.reset} — auto-buying...`);
|
|
1004
|
+
const boughtItems = [];
|
|
1005
|
+
for (const item of earlyMissingItems) {
|
|
1006
|
+
const bought = await tryBuyFarmItem({ missing: item, channel, waitForDankMemer, client });
|
|
1007
|
+
if (!bought.ok) {
|
|
1008
|
+
LOG.warn(`[farm] Could not buy ${item} (insufficient coins or unavailable) — retrying in 1h`);
|
|
1009
|
+
return { result: `need ${item} (buy failed)`, coins: 0, nextCooldownSec: 3600, skipReason: 'farm_missing_item' };
|
|
1010
|
+
}
|
|
1011
|
+
boughtItems.push(bought.itemName);
|
|
1012
|
+
}
|
|
1013
|
+
LOG.success(`[farm] Bought: ${boughtItems.join(', ')}. Retrying farm flow...`);
|
|
1014
|
+
await sleep(1200);
|
|
1015
|
+
if (_buyRetryDepth < 1) {
|
|
1016
|
+
return runFarm({ channel, waitForDankMemer, client, _buyRetryDepth: _buyRetryDepth + 1, _visionRetryDepth });
|
|
1017
|
+
}
|
|
1018
|
+
return { result: `auto-bought ${boughtItems.join(', ')}`, coins: 0, nextCooldownSec: 10 };
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (lower.includes('must specify a subcommand')) {
|
|
1022
|
+
LOG.warn('[farm] Subcommand required response detected; retrying with "pls farm view"');
|
|
1023
|
+
await channel.send('pls farm view');
|
|
1024
|
+
const retry = await waitForDankMemer(12000);
|
|
1025
|
+
if (!retry) return { result: 'no response after farm view retry', coins: 0 };
|
|
1026
|
+
response = retry;
|
|
1027
|
+
if (isCV2(response)) await ensureCV2(response);
|
|
1028
|
+
logMsg(response, 'farm-retry-view');
|
|
1029
|
+
logFarmState('retry-view', response);
|
|
1030
|
+
logFarmDeepState('retry-view', response);
|
|
1031
|
+
text = getFullText(response);
|
|
1032
|
+
clean = brief(text, 600);
|
|
1033
|
+
farmVision = analyzeFarmState({ msg: response, text });
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const cd = parseFarmCooldownSec(text);
|
|
1037
|
+
if (cd || lower.includes('already farmed') || lower.includes('farm again') || lower.includes('on cooldown')) {
|
|
1038
|
+
const sec = cd || 10;
|
|
1039
|
+
return { result: `farm cooldown (${Math.ceil(sec / 60)}m)`, coins: 0, nextCooldownSec: sec };
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Queue grow phase: if crops are still growing, wait until next ready window.
|
|
1043
|
+
const growReadySec = parseFarmGrowReadySec(text);
|
|
1044
|
+
LOG.info(`[farm] grow-ready parse=${growReadySec == null ? 'none' : `${growReadySec}s`}`);
|
|
1045
|
+
if (growReadySec && growReadySec > 20) {
|
|
1046
|
+
// Keep image-first diagnostics even while queuing.
|
|
1047
|
+
await inferPreferredActionFromImage(response);
|
|
1048
|
+
const waitSec = Math.min(6 * 3600, growReadySec + 2);
|
|
1049
|
+
LOG.info(`[farm] crops growing; queuing harvest check in ${waitSec}s`);
|
|
1050
|
+
return {
|
|
1051
|
+
result: `farm grow queue (${Math.ceil(waitSec / 60)}m)` ,
|
|
1052
|
+
coins: 0,
|
|
1053
|
+
nextCooldownSec: waitSec,
|
|
1054
|
+
skipReason: 'farm_grow_queue',
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Step 1: go to farm manage mode first (where Hoe/Water/Plant/etc live)
|
|
1059
|
+
const manageBtn = findFarmButton(response, ['manage', 'farm-farm:manage']);
|
|
1060
|
+
if (manageBtn) {
|
|
1061
|
+
LOG.info('[farm] Opening Manage menu');
|
|
1062
|
+
await humanDelay(90, 260);
|
|
1063
|
+
try {
|
|
1064
|
+
const managed = await clickAndCapture({
|
|
1065
|
+
channel,
|
|
1066
|
+
waitForDankMemer,
|
|
1067
|
+
response,
|
|
1068
|
+
button: manageBtn,
|
|
1069
|
+
tag: 'farm-manage',
|
|
1070
|
+
});
|
|
1071
|
+
if (managed) {
|
|
1072
|
+
response = managed;
|
|
1073
|
+
text = getFullText(response);
|
|
1074
|
+
clean = brief(text, 600);
|
|
1075
|
+
farmVision = analyzeFarmState({ msg: response, text });
|
|
1076
|
+
LOG.info(`[farm:vision] stage=${farmVision.stage}${farmVision.seedStock ? ` seed=${farmVision.seedStock.itemName}:${farmVision.seedStock.qty}` : ''}${farmVision.missing ? ` missing=${farmVision.missing}` : ''}`);
|
|
1077
|
+
logFarmDeepState('after-manage', response);
|
|
1078
|
+
}
|
|
1079
|
+
} catch (e) {
|
|
1080
|
+
LOG.error(`[farm] Manage click failed: ${e.message}`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Step 2: image-first phase suggestion. If we already know a target action,
|
|
1085
|
+
// skip deep multi-action audit to avoid touching every phase every run.
|
|
1086
|
+
const imageSuggestedAction = await inferPreferredActionFromImage(response);
|
|
1087
|
+
const lowerNow = String(stripAnsi(text || '')).toLowerCase();
|
|
1088
|
+
let textSuggestedAction = null;
|
|
1089
|
+
if (/seems pretty empty|pretty empty|empty\.{0,3}/i.test(lowerNow)) {
|
|
1090
|
+
textSuggestedAction = 'hoe';
|
|
1091
|
+
} else if (/ready to harvest|harvest ready|can be harvested|wilt/i.test(lowerNow)) {
|
|
1092
|
+
textSuggestedAction = 'harvest';
|
|
1093
|
+
}
|
|
1094
|
+
const harvestTextReady = /ready to harvest|can be harvested|harvest ready|wilt/i.test(String(stripAnsi(text || '')).toLowerCase());
|
|
1095
|
+
const effectivePreferredAction = _preferredAction || textSuggestedAction || (harvestTextReady ? 'harvest' : imageSuggestedAction) || null;
|
|
1096
|
+
|
|
1097
|
+
if (!effectivePreferredAction) {
|
|
1098
|
+
const audited = await auditManageAndBuildBuyPlan({ response, channel, waitForDankMemer, client });
|
|
1099
|
+
if (audited?.response) {
|
|
1100
|
+
response = audited.response;
|
|
1101
|
+
text = getFullText(response);
|
|
1102
|
+
clean = brief(text, 600);
|
|
1103
|
+
}
|
|
1104
|
+
if ((audited?.buyPlan || []).length > 0) {
|
|
1105
|
+
LOG.info(`[farm] audit buy plan: ${(audited.buyPlan || []).map(p => `${p.item} x${p.qty}`).join(', ')}`);
|
|
1106
|
+
const batch = await buyItemsBatch({
|
|
1107
|
+
channel,
|
|
1108
|
+
waitForDankMemer,
|
|
1109
|
+
client,
|
|
1110
|
+
items: audited.buyPlan.map(s => ({ itemName: s.item, quantity: s.qty })),
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
if (!batch.ok) {
|
|
1114
|
+
const failed = (batch.results || []).find(r => !r.success);
|
|
1115
|
+
const fItem = failed?.itemName || audited.buyPlan[0]?.item || 'item';
|
|
1116
|
+
LOG.warn(`[farm] Batch buy failed for ${fItem} — retrying in 1h`);
|
|
1117
|
+
return { result: `need ${fItem} (buy failed)`, coins: 0, nextCooldownSec: 3600, skipReason: 'farm_audit_restock_failed' };
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
for (const r of (batch.results || [])) {
|
|
1121
|
+
if (r.success) LOG.success(`[farm] Bought ${r.itemName} (${r.reason || 'ok'})`);
|
|
1122
|
+
}
|
|
1123
|
+
LOG.success('[farm] audit restock complete, retrying farm flow...');
|
|
1124
|
+
await sleep(1200);
|
|
1125
|
+
if (_buyRetryDepth < 2) {
|
|
1126
|
+
return runFarm({ channel, waitForDankMemer, client, _buyRetryDepth: _buyRetryDepth + 1, _visionRetryDepth, _preferredAction: effectivePreferredAction });
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
} else {
|
|
1130
|
+
LOG.info(`[farm] skipping deep audit; phase-directed flow using action=${effectivePreferredAction}`);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Step 3: select menu if farm presents crop/options
|
|
1134
|
+
const menus = getAllSelectMenus(response);
|
|
1135
|
+
if (menus.length > 0) {
|
|
1136
|
+
const menu = menus[0];
|
|
1137
|
+
const options = (menu.options || []).filter(o => !o.default);
|
|
1138
|
+
if (options.length > 0) {
|
|
1139
|
+
const pick = options[Math.floor(Math.random() * options.length)];
|
|
1140
|
+
LOG.info(`[farm] Selecting option: "${pick.label}"`);
|
|
1141
|
+
try {
|
|
1142
|
+
await response.selectMenu(menu.customId || menu.custom_id || 0, [pick.value]);
|
|
1143
|
+
const upd = await waitForDankMemer(7000);
|
|
1144
|
+
if (upd) {
|
|
1145
|
+
response = upd;
|
|
1146
|
+
if (isCV2(response)) await ensureCV2(response);
|
|
1147
|
+
logMsg(response, 'farm-after-select');
|
|
1148
|
+
logFarmState('after-select', response);
|
|
1149
|
+
logFarmDeepState('after-select', response);
|
|
1150
|
+
text = getFullText(response);
|
|
1151
|
+
clean = brief(text, 600);
|
|
1152
|
+
farmVision = analyzeFarmState({ msg: response, text });
|
|
1153
|
+
LOG.info(`[farm:vision] stage=${farmVision.stage}${farmVision.seedStock ? ` seed=${farmVision.seedStock.itemName}:${farmVision.seedStock.qty}` : ''}${farmVision.missing ? ` missing=${farmVision.missing}` : ''}`);
|
|
1154
|
+
}
|
|
1155
|
+
} catch (e) {
|
|
1156
|
+
LOG.error(`[farm] Select failed: ${e.message}`);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Step 4: choose and click farm action (harvest/hoe/water/plant/...)
|
|
1162
|
+
const countRestock = await restockFromManageCounts({ response, channel, waitForDankMemer, client });
|
|
1163
|
+
if (countRestock.failed) {
|
|
1164
|
+
return countRestock;
|
|
1165
|
+
}
|
|
1166
|
+
if (countRestock.boughtAny) {
|
|
1167
|
+
LOG.success('[farm] Manage restock complete. Retrying farm flow...');
|
|
1168
|
+
await sleep(1200);
|
|
1169
|
+
if (_buyRetryDepth < 2) {
|
|
1170
|
+
return runFarm({ channel, waitForDankMemer, client, _buyRetryDepth: _buyRetryDepth + 1, _visionRetryDepth });
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const allButtons = getAllButtons(response).filter(b => !b.disabled);
|
|
1175
|
+
const missingAfterManage = parseMissingFarmItems(text);
|
|
1176
|
+
if (missingAfterManage.length > 0) {
|
|
1177
|
+
LOG.warn(`[farm] Missing in manage flow: ${c.bold}${missingAfterManage.join(', ')}${c.reset} — auto-buying...`);
|
|
1178
|
+
const boughtItems = [];
|
|
1179
|
+
for (const item of missingAfterManage) {
|
|
1180
|
+
const bought = await tryBuyFarmItem({ missing: item, channel, waitForDankMemer, client });
|
|
1181
|
+
if (!bought.ok) {
|
|
1182
|
+
LOG.warn(`[farm] Could not buy ${item} — retrying in 1h`);
|
|
1183
|
+
return { result: `need ${item} (buy failed)`, coins: 0, nextCooldownSec: 3600, skipReason: 'farm_missing_item' };
|
|
1184
|
+
}
|
|
1185
|
+
boughtItems.push(bought.itemName);
|
|
1186
|
+
}
|
|
1187
|
+
LOG.success(`[farm] Bought: ${boughtItems.join(', ')}. Retrying farm flow...`);
|
|
1188
|
+
await sleep(1200);
|
|
1189
|
+
if (_buyRetryDepth < 1) {
|
|
1190
|
+
return runFarm({ channel, waitForDankMemer, client, _buyRetryDepth: _buyRetryDepth + 1, _visionRetryDepth });
|
|
1191
|
+
}
|
|
1192
|
+
return { result: `auto-bought ${boughtItems.join(', ')}`, coins: 0, nextCooldownSec: 10 };
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const preferredActions = getManageActionButtons(response);
|
|
1196
|
+
const preferredBtn = effectivePreferredAction ? preferredActions[effectivePreferredAction] : null;
|
|
1197
|
+
const pickedAction = preferredBtn
|
|
1198
|
+
? { action: effectivePreferredAction, button: preferredBtn }
|
|
1199
|
+
: pickFarmActionButton(response, text);
|
|
1200
|
+
const actionBtn = pickedAction?.button || allButtons.find(isFarmActionButton) || null;
|
|
1201
|
+
const actionName = pickedAction?.action || 'action';
|
|
1202
|
+
LOG.info(`[farm] action decision actionName=${actionName} preferred=${effectivePreferredAction || '-'} btn="${actionBtn?.label || '-'}"`);
|
|
1203
|
+
|
|
1204
|
+
if (!actionBtn) {
|
|
1205
|
+
const labels = allButtons.map(b => (b.label || '-')).filter(Boolean);
|
|
1206
|
+
const mgmtOnly = allButtons.length > 0 && allButtons.every(isFarmManagementButton);
|
|
1207
|
+
if (labels.length > 0) {
|
|
1208
|
+
LOG.info(`[farm] No actionable farm button found. visible=[${labels.join(', ')}]`);
|
|
1209
|
+
}
|
|
1210
|
+
const fallbackCd = mgmtOnly ? 300 : 60;
|
|
1211
|
+
return {
|
|
1212
|
+
result: mgmtOnly ? 'farm setup/management only (no crop actions yet)' : (clean || 'farm no actionable buttons'),
|
|
1213
|
+
coins: 0,
|
|
1214
|
+
nextCooldownSec: fallbackCd,
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
let phaseScorePreApply = null;
|
|
1219
|
+
|
|
1220
|
+
LOG.info(`[farm] Clicking ${actionName}: "${actionBtn.label || '?'}"`);
|
|
1221
|
+
await humanDelay(100, 280);
|
|
1222
|
+
try {
|
|
1223
|
+
const post = await clickAndCapture({
|
|
1224
|
+
channel,
|
|
1225
|
+
waitForDankMemer,
|
|
1226
|
+
response,
|
|
1227
|
+
button: actionBtn,
|
|
1228
|
+
tag: 'farm-followup',
|
|
1229
|
+
timeoutMs: 10000,
|
|
1230
|
+
});
|
|
1231
|
+
if (post) {
|
|
1232
|
+
response = post;
|
|
1233
|
+
text = getFullText(response);
|
|
1234
|
+
clean = brief(text, 600);
|
|
1235
|
+
farmVision = analyzeFarmState({ msg: response, text });
|
|
1236
|
+
LOG.info(`[farm:vision] stage=${farmVision.stage}${farmVision.seedStock ? ` seed=${farmVision.seedStock.itemName}:${farmVision.seedStock.qty}` : ''}${farmVision.missing ? ` missing=${farmVision.missing}` : ''}`);
|
|
1237
|
+
logFarmDeepState('after-action', response);
|
|
1238
|
+
|
|
1239
|
+
// Phase checkpoint #1: image score before applying "All".
|
|
1240
|
+
phaseScorePreApply = await capturePhaseVisionScore({
|
|
1241
|
+
msg: response,
|
|
1242
|
+
actionName,
|
|
1243
|
+
phaseTag: `${actionName}-pre-apply`,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
} catch (e) {
|
|
1247
|
+
LOG.error(`[farm] Click failed: ${e.message}`);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Seed top-up: if we know selected seed quantity and it's below 9 slots, buy only deficit.
|
|
1251
|
+
if (actionName === 'plant') {
|
|
1252
|
+
const seedInfo = farmVision.seedStock;
|
|
1253
|
+
if (seedInfo && Number.isFinite(seedInfo.qty)) {
|
|
1254
|
+
const deficit = Math.max(0, FARM_TOTAL_SLOTS - seedInfo.qty);
|
|
1255
|
+
if (deficit > 0) {
|
|
1256
|
+
LOG.warn(`[farm] ${seedInfo.itemName}: have ${seedInfo.qty}, need ${FARM_TOTAL_SLOTS} → buying ${deficit}`);
|
|
1257
|
+
const bought = await buyItem({ channel, waitForDankMemer, client, itemName: seedInfo.itemName, quantity: deficit });
|
|
1258
|
+
if (!bought) {
|
|
1259
|
+
LOG.warn(`[farm] Could not buy ${deficit}x ${seedInfo.itemName} — retrying in 1h`);
|
|
1260
|
+
return { result: `need ${seedInfo.itemName} +${deficit} (buy failed)`, coins: 0, nextCooldownSec: 3600, skipReason: 'farm_seed_deficit' };
|
|
1261
|
+
}
|
|
1262
|
+
LOG.success(`[farm] Bought ${deficit}x ${seedInfo.itemName}. Retrying farm flow...`);
|
|
1263
|
+
await sleep(1200);
|
|
1264
|
+
if (_buyRetryDepth < 2) {
|
|
1265
|
+
return runFarm({ channel, waitForDankMemer, client, _buyRetryDepth: _buyRetryDepth + 1, _visionRetryDepth });
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// Step 5: in plant phase, choose a seed in dropdown first (enables Plant All in many states)
|
|
1272
|
+
if (actionName === 'plant') {
|
|
1273
|
+
const seedPick = await ensurePlantSeedSelected({ response, waitForDankMemer, channel });
|
|
1274
|
+
if (seedPick?.response) response = seedPick.response;
|
|
1275
|
+
LOG.info(`[farm:plant] seed selection result selected=${!!seedPick?.selected} reason=${seedPick?.reason || 'unknown'}`);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Step 6: for action prompts, click ALL to apply on all tiles (preferred over pick slots)
|
|
1279
|
+
const allBtn = findFarmButton(response, [
|
|
1280
|
+
' all', 'all ', 'all)', 'all:', ':all', 'hoe all', 'water all', 'plant all', 'harvest all', 'confirm all',
|
|
1281
|
+
]);
|
|
1282
|
+
const pickSlotsBtn = findFarmButton(response, ['pick slot', 'pick slots']);
|
|
1283
|
+
const disabledAllBtn = getDisabledAllButtonForAction(response, actionName);
|
|
1284
|
+
LOG.info(`[farm] apply decision allBtn="${allBtn?.label || '-'}" pickSlots="${pickSlotsBtn?.label || '-'}"`);
|
|
1285
|
+
if (allBtn) {
|
|
1286
|
+
LOG.info(`[farm] Applying action with "${allBtn.label || '?'}"`);
|
|
1287
|
+
await humanDelay(90, 260);
|
|
1288
|
+
try {
|
|
1289
|
+
const applied = await clickAndCapture({
|
|
1290
|
+
channel,
|
|
1291
|
+
waitForDankMemer,
|
|
1292
|
+
response,
|
|
1293
|
+
button: allBtn,
|
|
1294
|
+
tag: 'farm-apply-all',
|
|
1295
|
+
});
|
|
1296
|
+
if (applied) {
|
|
1297
|
+
response = applied;
|
|
1298
|
+
text = getFullText(response);
|
|
1299
|
+
clean = brief(text, 600);
|
|
1300
|
+
farmVision = analyzeFarmState({ msg: response, text });
|
|
1301
|
+
LOG.info(`[farm:vision] stage=${farmVision.stage}${farmVision.seedStock ? ` seed=${farmVision.seedStock.itemName}:${farmVision.seedStock.qty}` : ''}${farmVision.missing ? ` missing=${farmVision.missing}` : ''}`);
|
|
1302
|
+
logFarmDeepState('after-apply', response);
|
|
1303
|
+
|
|
1304
|
+
// Image + score verification: repeat once and repair missing tools/seeds if score still fails.
|
|
1305
|
+
if (['hoe', 'water', 'plant', 'harvest'].includes(actionName)) {
|
|
1306
|
+
const imgCheck = await capturePhaseVisionScore({
|
|
1307
|
+
msg: response,
|
|
1308
|
+
actionName,
|
|
1309
|
+
phaseTag: `${actionName}-post-apply`,
|
|
1310
|
+
});
|
|
1311
|
+
const score = imgCheck.score || null;
|
|
1312
|
+
const scoreMismatch = !!(score && !score.matched);
|
|
1313
|
+
const nextActionFromScore = inferNextActionFromScore(actionName, score);
|
|
1314
|
+
|
|
1315
|
+
if (scoreMismatch) {
|
|
1316
|
+
LOG.warn(`[farm:vision-score] action=${actionName} score=${score.score}/${score.threshold} conf=${score.confidence} reason=${score.reason} ratios=${JSON.stringify(score.ratios)}`);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
if (nextActionFromScore && _visionRetryDepth < 12) {
|
|
1320
|
+
LOG.info(`[farm] Vision suggests switching phase (${actionName} -> ${nextActionFromScore}) instead of retrying same action (depth=${_visionRetryDepth})`);
|
|
1321
|
+
await sleep(700);
|
|
1322
|
+
return runFarm({
|
|
1323
|
+
channel,
|
|
1324
|
+
waitForDankMemer,
|
|
1325
|
+
client,
|
|
1326
|
+
_buyRetryDepth,
|
|
1327
|
+
_visionRetryDepth: _visionRetryDepth + 1,
|
|
1328
|
+
_preferredAction: nextActionFromScore,
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
if (imgCheck.shouldRepeat || scoreMismatch) {
|
|
1333
|
+
const retryAllBtn = findFarmButton(response, [
|
|
1334
|
+
' all', 'all ', 'all)', 'all:', ':all',
|
|
1335
|
+
`${actionName} all`,
|
|
1336
|
+
'hoe all', 'water all', 'plant all', 'harvest all',
|
|
1337
|
+
]);
|
|
1338
|
+
if (retryAllBtn) {
|
|
1339
|
+
LOG.info(`[farm] image/score check says incomplete; re-applying with "${retryAllBtn.label || '?'}"`);
|
|
1340
|
+
const reapplied = await clickAndCapture({
|
|
1341
|
+
channel,
|
|
1342
|
+
waitForDankMemer,
|
|
1343
|
+
response,
|
|
1344
|
+
button: retryAllBtn,
|
|
1345
|
+
tag: 'farm-apply-all-repeat',
|
|
1346
|
+
});
|
|
1347
|
+
if (reapplied) {
|
|
1348
|
+
response = reapplied;
|
|
1349
|
+
text = getFullText(response);
|
|
1350
|
+
clean = brief(text, 600);
|
|
1351
|
+
farmVision = analyzeFarmState({ msg: response, text });
|
|
1352
|
+
logFarmDeepState('after-apply-repeat', response);
|
|
1353
|
+
|
|
1354
|
+
const imgCheck2 = await capturePhaseVisionScore({
|
|
1355
|
+
msg: response,
|
|
1356
|
+
actionName,
|
|
1357
|
+
phaseTag: `${actionName}-post-repeat`,
|
|
1358
|
+
});
|
|
1359
|
+
const score2 = imgCheck2.score || null;
|
|
1360
|
+
if (score2 && !score2.matched) {
|
|
1361
|
+
LOG.warn(`[farm:vision-score] post-repeat mismatch action=${actionName} score=${score2.score}/${score2.threshold} conf=${score2.confidence} reason=${score2.reason}`);
|
|
1362
|
+
|
|
1363
|
+
if (actionName === 'hoe') {
|
|
1364
|
+
const tilled2 = score2.ratios?.tilled || 0;
|
|
1365
|
+
const progressed2 = (score2.ratios?.planted || 0) + (score2.ratios?.wet || 0);
|
|
1366
|
+
if (tilled2 < 0.6 && progressed2 < 0.25) {
|
|
1367
|
+
LOG.warn('[farm] Hoe did not till the whole farm; hoe is likely broken or missing.');
|
|
1368
|
+
return {
|
|
1369
|
+
result: 'need hoe (hoe not tilling whole farm)',
|
|
1370
|
+
coins: 0,
|
|
1371
|
+
nextCooldownSec: 3600,
|
|
1372
|
+
skipReason: 'farm_hoe_broken',
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const nextAction = inferNextActionFromScore(actionName, score2);
|
|
1378
|
+
if (nextAction && _visionRetryDepth < 12) {
|
|
1379
|
+
LOG.info(`[farm] Vision indicates phase already advanced (${actionName} -> ${nextAction}); retrying with preferred action ${nextAction} (depth=${_visionRetryDepth})`);
|
|
1380
|
+
await sleep(700);
|
|
1381
|
+
return runFarm({
|
|
1382
|
+
channel,
|
|
1383
|
+
waitForDankMemer,
|
|
1384
|
+
client,
|
|
1385
|
+
_buyRetryDepth,
|
|
1386
|
+
_visionRetryDepth: _visionRetryDepth + 1,
|
|
1387
|
+
_preferredAction: nextAction,
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const repair = getVisionRepairPlan(actionName, score2);
|
|
1392
|
+
if (repair && _visionRetryDepth < 5) {
|
|
1393
|
+
LOG.warn(`[farm] Vision mismatch repair: buying ${repair.label} x${repair.quantity} and retrying`);
|
|
1394
|
+
const bought = await buyItem({
|
|
1395
|
+
channel,
|
|
1396
|
+
waitForDankMemer,
|
|
1397
|
+
client,
|
|
1398
|
+
itemName: repair.itemName,
|
|
1399
|
+
quantity: repair.quantity,
|
|
1400
|
+
});
|
|
1401
|
+
if (!bought) {
|
|
1402
|
+
LOG.warn(`[farm] Could not buy vision-repair item ${repair.label} x${repair.quantity} — retrying in 1h`);
|
|
1403
|
+
return {
|
|
1404
|
+
result: `need ${repair.label} x${repair.quantity} (vision mismatch buy failed)`,
|
|
1405
|
+
coins: 0,
|
|
1406
|
+
nextCooldownSec: 3600,
|
|
1407
|
+
skipReason: 'farm_vision_score_repair_failed',
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
await sleep(1100);
|
|
1411
|
+
return runFarm({
|
|
1412
|
+
channel,
|
|
1413
|
+
waitForDankMemer,
|
|
1414
|
+
client,
|
|
1415
|
+
_buyRetryDepth: Math.min(_buyRetryDepth + 1, 2),
|
|
1416
|
+
_visionRetryDepth: _visionRetryDepth + 1,
|
|
1417
|
+
_preferredAction: null,
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
const extraText = String(stripAnsi(getFullText(response?._farmExtraInteraction) || response?._farmExtraInteraction?.content || '')).toLowerCase();
|
|
1427
|
+
if (actionName === 'plant' && /only\s+plant\s+seeds\s+on\s+an\s+empty\s+tile|tilled\s+and\s+watered/.test(extraText)) {
|
|
1428
|
+
LOG.warn('[farm] Plant was rejected by Dank (needs empty+tilled+watered). Restarting phase flow at Hoe.');
|
|
1429
|
+
if (_visionRetryDepth < 12) {
|
|
1430
|
+
await sleep(700);
|
|
1431
|
+
return runFarm({
|
|
1432
|
+
channel,
|
|
1433
|
+
waitForDankMemer,
|
|
1434
|
+
client,
|
|
1435
|
+
_buyRetryDepth,
|
|
1436
|
+
_visionRetryDepth: _visionRetryDepth + 1,
|
|
1437
|
+
_preferredAction: 'hoe',
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
if (actionName === 'hoe' && /can only use .*hoe.*empty tile|after a harvest/.test(extraText)) {
|
|
1443
|
+
LOG.warn('[farm] Extra interaction says hoe can only be used on empty tiles; moving to Water phase');
|
|
1444
|
+
if (_visionRetryDepth < 5) {
|
|
1445
|
+
await sleep(700);
|
|
1446
|
+
return runFarm({
|
|
1447
|
+
channel,
|
|
1448
|
+
waitForDankMemer,
|
|
1449
|
+
client,
|
|
1450
|
+
_buyRetryDepth,
|
|
1451
|
+
_visionRetryDepth: _visionRetryDepth + 1,
|
|
1452
|
+
_preferredAction: 'water',
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
} catch (e) {
|
|
1458
|
+
LOG.error(`[farm] All click failed: ${e.message}`);
|
|
1459
|
+
}
|
|
1460
|
+
} else if (pickSlotsBtn) {
|
|
1461
|
+
if (disabledAllBtn) {
|
|
1462
|
+
if (actionName === 'plant') {
|
|
1463
|
+
const preScore = phaseScorePreApply?.score || null;
|
|
1464
|
+
const plantedRatio = preScore?.ratios?.planted || 0;
|
|
1465
|
+
if ((preScore && preScore.matched) || plantedRatio >= 0.6) {
|
|
1466
|
+
LOG.info('[farm] Plant All disabled but image shows planted state; skipping seed rebuy and moving to harvest phase');
|
|
1467
|
+
if (_visionRetryDepth < 12) {
|
|
1468
|
+
await sleep(700);
|
|
1469
|
+
return runFarm({
|
|
1470
|
+
channel,
|
|
1471
|
+
waitForDankMemer,
|
|
1472
|
+
client,
|
|
1473
|
+
_buyRetryDepth,
|
|
1474
|
+
_visionRetryDepth: _visionRetryDepth + 1,
|
|
1475
|
+
_preferredAction: 'harvest',
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
LOG.warn(`[farm] ${disabledAllBtn.label} is disabled while Pick Slots exists; inferring missing requirement for ${actionName}`);
|
|
1482
|
+
let inferredItem = null;
|
|
1483
|
+
if (actionName === 'hoe') inferredItem = 'hoe';
|
|
1484
|
+
else if (actionName === 'water') inferredItem = 'watering can';
|
|
1485
|
+
else if (actionName === 'plant') inferredItem = 'seeds';
|
|
1486
|
+
|
|
1487
|
+
if (inferredItem) {
|
|
1488
|
+
const qty = inferredItem === 'seeds' ? FARM_TOTAL_SLOTS : 1;
|
|
1489
|
+
if (actionName === 'hoe') {
|
|
1490
|
+
LOG.warn('[farm] Hoe All is not usable / full till failed — hoe is likely broken or missing.');
|
|
1491
|
+
}
|
|
1492
|
+
const ok = await buyItem({ channel, waitForDankMemer, client, itemName: inferredItem, quantity: qty });
|
|
1493
|
+
if (!ok) {
|
|
1494
|
+
LOG.warn(`[farm] Could not buy inferred requirement ${inferredItem} x${qty} — retrying in 1h`);
|
|
1495
|
+
return { result: `need ${inferredItem} x${qty} (buy failed)`, coins: 0, nextCooldownSec: 3600, skipReason: 'farm_inferred_missing' };
|
|
1496
|
+
}
|
|
1497
|
+
LOG.success(`[farm] Bought inferred requirement ${inferredItem} x${qty}. Retrying farm flow...`);
|
|
1498
|
+
await sleep(1200);
|
|
1499
|
+
if (_buyRetryDepth < 2) {
|
|
1500
|
+
return runFarm({ channel, waitForDankMemer, client, _buyRetryDepth: _buyRetryDepth + 1, _visionRetryDepth });
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
LOG.info('[farm] Only "Pick Slots" available; skipping slot-specific interaction this tick');
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const coins = parseCoins(text);
|
|
1508
|
+
let nextCd = parseFarmCooldownSec(text) || 10;
|
|
1509
|
+
const missingEnd = parseMissingFarmItem(text);
|
|
1510
|
+
if (missingEnd) {
|
|
1511
|
+
LOG.warn(`[farm] Missing ${c.bold}${missingEnd}${c.reset} after action — will retry in 1h`);
|
|
1512
|
+
return { result: `need ${missingEnd}`, coins: 0, nextCooldownSec: 3600, skipReason: 'farm_missing_item' };
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
if (coins <= 0) {
|
|
1516
|
+
const finalVision = analyzeFarmState({ msg: response, text });
|
|
1517
|
+
if (finalVision.stage === 'overview' || finalVision.stage === 'blocked-missing-item') {
|
|
1518
|
+
nextCd = Math.max(nextCd, 300);
|
|
1519
|
+
}
|
|
1520
|
+
if (/seems pretty empty|pretty empty|farm #\d+/i.test(String(stripAnsi(text || '')))) {
|
|
1521
|
+
nextCd = Math.max(nextCd, 300);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// Phase checkpoint #final: final image score for this action tick.
|
|
1526
|
+
await capturePhaseVisionScore({
|
|
1527
|
+
msg: response,
|
|
1528
|
+
actionName,
|
|
1529
|
+
phaseTag: `${actionName}-final`,
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
if (coins > 0) {
|
|
1533
|
+
LOG.coin(`[farm] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
1534
|
+
return { result: `farm ${actionName} → +⏣ ${coins.toLocaleString()}`, coins, nextCooldownSec: nextCd };
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
return { result: clean || 'farm done', coins: 0, nextCooldownSec: nextCd };
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
module.exports = { runFarm };
|