dankgrinder 4.0.0 → 4.1.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/README.md +25 -16
- package/bin/dankgrinder.js +18 -28
- package/lib/commands/adventure.js +502 -0
- package/lib/commands/beg.js +45 -0
- package/lib/commands/blackjack.js +85 -0
- package/lib/commands/crime.js +94 -0
- package/lib/commands/deposit.js +46 -0
- package/lib/commands/dig.js +82 -0
- package/lib/commands/fish.js +615 -0
- package/lib/commands/fishVision.js +141 -0
- package/lib/commands/gamble.js +96 -0
- package/lib/commands/generic.js +181 -0
- package/lib/commands/highlow.js +112 -0
- package/lib/commands/hunt.js +85 -0
- package/lib/commands/index.js +59 -0
- package/lib/commands/postmemes.js +148 -0
- package/lib/commands/profile.js +99 -0
- package/lib/commands/scratch.js +83 -0
- package/lib/commands/search.js +102 -0
- package/lib/commands/shop.js +262 -0
- package/lib/commands/trivia.js +146 -0
- package/lib/commands/utils.js +287 -0
- package/lib/commands/work.js +400 -0
- package/lib/grinder.js +560 -656
- package/package.json +4 -3
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fish command handler (Dank Memer v2 fishing).
|
|
3
|
+
*
|
|
4
|
+
* The fishing minigame works as follows:
|
|
5
|
+
* 1. "pls fish catch" → fishing dashboard with "Go Fishing" button
|
|
6
|
+
* 2. Click "Go Fishing" → 3x3 grid image + 9 "Catch" buttons
|
|
7
|
+
* 3. Grid image shows mines (dark patches) and safe water
|
|
8
|
+
* 4. Click safe "Catch" buttons to catch fish, avoid mines
|
|
9
|
+
* 5. Result screen with "Fish Again" / "Go Back" buttons
|
|
10
|
+
* 6. Click "Fish Again" to loop without re-sending the command
|
|
11
|
+
*
|
|
12
|
+
* Image analysis uses sharp to detect dark pixel clusters (mines)
|
|
13
|
+
* in each grid cell. Safe cells have ~0% dark pixels; mine cells
|
|
14
|
+
* have >6% dark pixels with lower average brightness.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
|
|
19
|
+
logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
|
|
20
|
+
} = require('./utils');
|
|
21
|
+
const { downloadImage, extractImageUrl, findSafeCells } = require('./fishVision');
|
|
22
|
+
|
|
23
|
+
const { DANK_MEMER_ID } = require('./utils');
|
|
24
|
+
|
|
25
|
+
async function refetchMsg(channel, msgId) {
|
|
26
|
+
try { return await channel.messages.fetch(msgId); } catch { return null; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sell all fish from buckets via "pls fish buckets" → "Sell All Fish" button.
|
|
31
|
+
* The sell flow has a confirmation step with "Sell for X Coins" / "Sell for Y Tokens".
|
|
32
|
+
* Clicking "Sell All Fish" edits the message to show confirmation, then we click the token sell.
|
|
33
|
+
* @param {string} [sellFor='tokens'] - 'tokens' or 'coins'
|
|
34
|
+
* @returns {Promise<boolean>} true if sold successfully
|
|
35
|
+
*/
|
|
36
|
+
async function sellAllFish({ channel, waitForDankMemer, sellFor = 'tokens' }) {
|
|
37
|
+
LOG.info('[fish] Selling all fish from buckets...');
|
|
38
|
+
await channel.send('pls fish buckets');
|
|
39
|
+
let msg = await waitForDankMemer(8000);
|
|
40
|
+
if (!msg) { LOG.warn('[fish] No response to pls fish buckets'); return false; }
|
|
41
|
+
let fresh = await refetchMsg(channel, msg.id);
|
|
42
|
+
if (fresh) msg = fresh;
|
|
43
|
+
|
|
44
|
+
const sellAllBtn = getAllButtons(msg).find(b =>
|
|
45
|
+
!b.disabled && (b.customId || '').includes('fish-buckets-sell-all')
|
|
46
|
+
);
|
|
47
|
+
if (!sellAllBtn) {
|
|
48
|
+
LOG.info('[fish] No "Sell All Fish" button (bucket may be empty)');
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Click "Sell All Fish" — this edits the message to show confirmation
|
|
53
|
+
await humanDelay(80, 200);
|
|
54
|
+
try {
|
|
55
|
+
await safeClickButton(msg, sellAllBtn);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
LOG.error(`[fish] Sell All Fish click error: ${e.message}`);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Wait for confirmation screen (message update with "Sell for X Coins/Tokens")
|
|
62
|
+
await sleep(1500);
|
|
63
|
+
let confirmMsg = null;
|
|
64
|
+
// Check for update event first
|
|
65
|
+
for (let i = 0; i < 4; i++) {
|
|
66
|
+
const upd = await waitForDankMemer(3000);
|
|
67
|
+
if (!upd) break;
|
|
68
|
+
const btns = getAllButtons(upd);
|
|
69
|
+
if (btns.some(b => (b.label || '').toLowerCase().includes('sell for'))) {
|
|
70
|
+
confirmMsg = upd;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Fallback: refetch the message
|
|
75
|
+
if (!confirmMsg) {
|
|
76
|
+
confirmMsg = await refetchMsg(channel, msg.id);
|
|
77
|
+
}
|
|
78
|
+
if (!confirmMsg) { LOG.warn('[fish] No sell confirmation'); return false; }
|
|
79
|
+
|
|
80
|
+
// Find the sell confirmation button
|
|
81
|
+
const confirmBtns = getAllButtons(confirmMsg);
|
|
82
|
+
let sellBtn;
|
|
83
|
+
if (sellFor === 'tokens') {
|
|
84
|
+
// Prefer "Sell for X Tokens" button
|
|
85
|
+
sellBtn = confirmBtns.find(b =>
|
|
86
|
+
!b.disabled && (b.label || '').toLowerCase().includes('sell for') && (b.label || '').toLowerCase().includes('token')
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (!sellBtn) {
|
|
90
|
+
// Fallback: "Sell for X Coins"
|
|
91
|
+
sellBtn = confirmBtns.find(b =>
|
|
92
|
+
!b.disabled && (b.label || '').toLowerCase().includes('sell for') && (b.label || '').toLowerCase().includes('coin')
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (!sellBtn) {
|
|
96
|
+
// Last fallback: any green/primary sell button
|
|
97
|
+
sellBtn = confirmBtns.find(b =>
|
|
98
|
+
!b.disabled && (b.label || '').toLowerCase().includes('sell for')
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!sellBtn) {
|
|
103
|
+
LOG.warn('[fish] No sell confirmation button found');
|
|
104
|
+
LOG.debug(`[fish] Buttons: ${confirmBtns.map(b => `"${b.label}"`).join(', ')}`);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
LOG.info(`[fish] Confirming: "${sellBtn.label}"`);
|
|
109
|
+
await humanDelay(80, 200);
|
|
110
|
+
try {
|
|
111
|
+
await safeClickButton(confirmMsg, sellBtn);
|
|
112
|
+
await sleep(1500);
|
|
113
|
+
// Drain events from sell
|
|
114
|
+
for (let d = 0; d < 3; d++) {
|
|
115
|
+
const ev = await waitForDankMemer(2000);
|
|
116
|
+
if (!ev) break;
|
|
117
|
+
}
|
|
118
|
+
LOG.success('[fish] Sold all fish from buckets');
|
|
119
|
+
return true;
|
|
120
|
+
} catch (e) {
|
|
121
|
+
LOG.error(`[fish] Sell confirmation error: ${e.message}`);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Parse cooldown timestamp from text.
|
|
128
|
+
*/
|
|
129
|
+
function parseFishCooldown(text) {
|
|
130
|
+
const m = text.match(/<t:(\d+):R>/);
|
|
131
|
+
if (m) {
|
|
132
|
+
const diff = parseInt(m[1]) - Math.floor(Date.now() / 1000);
|
|
133
|
+
return diff > 0 ? diff : null;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Wait for the Catch grid OR a direct result (e.g. "nothing to catch").
|
|
140
|
+
* After clicking Go Fishing / Fish Again, the animation goes:
|
|
141
|
+
* loading text (0 buttons) → EITHER Catch grid (9 btns) OR direct result (2 btns)
|
|
142
|
+
* Returns { type: 'grid', msg } or { type: 'result', msg } or null.
|
|
143
|
+
*/
|
|
144
|
+
async function waitForFishingOutcome({ channel, msgId, waitForDankMemer }) {
|
|
145
|
+
const hasCatch = (msg) => getAllButtons(msg).some(b => (b.label || '').toLowerCase() === 'catch');
|
|
146
|
+
const hasResult = (msg) => {
|
|
147
|
+
const btns = getAllButtons(msg);
|
|
148
|
+
return btns.some(b => (b.customId || '').includes('fish-catch-back')) ||
|
|
149
|
+
btns.some(b => (b.label || '').toLowerCase() === 'fish again');
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < 10; i++) {
|
|
153
|
+
const upd = await waitForDankMemer(2000);
|
|
154
|
+
if (upd) {
|
|
155
|
+
if (hasCatch(upd)) return { type: 'grid', msg: upd };
|
|
156
|
+
if (hasResult(upd)) return { type: 'result', msg: upd };
|
|
157
|
+
}
|
|
158
|
+
// Also check via refetch each iteration
|
|
159
|
+
const rf = await refetchMsg(channel, msgId);
|
|
160
|
+
if (rf) {
|
|
161
|
+
if (hasCatch(rf)) return { type: 'grid', msg: rf };
|
|
162
|
+
if (hasResult(rf)) return { type: 'result', msg: rf };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Wait for the result screen (Go Back / Fish Again buttons).
|
|
170
|
+
*/
|
|
171
|
+
async function waitForResult({ channel, msgId, waitForDankMemer }) {
|
|
172
|
+
for (let i = 0; i < 6; i++) {
|
|
173
|
+
const upd = await waitForDankMemer(5000);
|
|
174
|
+
if (!upd) {
|
|
175
|
+
const rf = await refetchMsg(channel, msgId);
|
|
176
|
+
if (rf) {
|
|
177
|
+
const btns = getAllButtons(rf);
|
|
178
|
+
const hasResult = btns.some(b => (b.customId || '').includes('fish-catch-back') || (b.label || '').toLowerCase() === 'fish again');
|
|
179
|
+
if (hasResult) return rf;
|
|
180
|
+
}
|
|
181
|
+
await sleep(2000);
|
|
182
|
+
return await refetchMsg(channel, msgId);
|
|
183
|
+
}
|
|
184
|
+
const btns = getAllButtons(upd);
|
|
185
|
+
const hasResult = btns.some(b => (b.customId || '').includes('fish-catch-back') || (b.label || '').toLowerCase() === 'fish again');
|
|
186
|
+
if (hasResult) return upd;
|
|
187
|
+
}
|
|
188
|
+
return await refetchMsg(channel, msgId);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Parse the result of a fishing round.
|
|
193
|
+
*/
|
|
194
|
+
function parseFishResult(text) {
|
|
195
|
+
const tl = text.toLowerCase();
|
|
196
|
+
const cd = parseFishCooldown(text);
|
|
197
|
+
const headerMatch = text.match(/###\s*(.+?)(?:\s*-#|$)/);
|
|
198
|
+
const header = headerMatch ? headerMatch[1].trim() : '';
|
|
199
|
+
|
|
200
|
+
if (tl.includes('nothing to catch') || tl.includes('no fish')) {
|
|
201
|
+
return { outcome: 'nothing', header, cd };
|
|
202
|
+
}
|
|
203
|
+
if (tl.includes('mine') || tl.includes('triggered')) {
|
|
204
|
+
return { outcome: 'mine', header, cd };
|
|
205
|
+
}
|
|
206
|
+
if (tl.includes('got away') || tl.includes('escaped')) {
|
|
207
|
+
return { outcome: 'got_away', header, cd };
|
|
208
|
+
}
|
|
209
|
+
if (tl.includes('caught')) {
|
|
210
|
+
const fishMatch = text.match(/caught\s+(?:a\s+)?(.+?)(?:\s*[!.]|$)/i);
|
|
211
|
+
return { outcome: 'caught', fish: fishMatch ? fishMatch[1].substring(0, 40) : 'a fish', header, cd };
|
|
212
|
+
}
|
|
213
|
+
return { outcome: 'unknown', header: header || text.substring(0, 60), cd };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Single fishing round: analyze grid, click safe cells, return result.
|
|
218
|
+
* @param {object} gameMsg - Message with MEDIA_GALLERY + Catch buttons
|
|
219
|
+
* @param {object} channel
|
|
220
|
+
* @param {string} msgId
|
|
221
|
+
* @param {function} waitForDankMemer
|
|
222
|
+
* @returns {Promise<{outcome, header, cd, clickedCount}>}
|
|
223
|
+
*/
|
|
224
|
+
async function playFishRound({ gameMsg, channel, msgId, waitForDankMemer }) {
|
|
225
|
+
// Extract and analyze the grid image
|
|
226
|
+
const imgUrl = extractImageUrl(gameMsg.components);
|
|
227
|
+
if (!imgUrl) {
|
|
228
|
+
LOG.warn('[fish] No grid image URL');
|
|
229
|
+
return { outcome: 'no_image', header: 'no image', cd: null, clickedCount: 0 };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let target = null;
|
|
233
|
+
try {
|
|
234
|
+
const imgBuf = await downloadImage(imgUrl);
|
|
235
|
+
const analysis = await findSafeCells(imgBuf);
|
|
236
|
+
const { fishCell, mineCells, allCells } = analysis;
|
|
237
|
+
|
|
238
|
+
// Log grid classification
|
|
239
|
+
const cellMap = allCells.map(c => `[${c.col},${c.row}]=${c.type}${c.type !== 'empty' ? `(fill=${c.fillRatio})` : ''}`);
|
|
240
|
+
LOG.info(`[fish] Grid: ${cellMap.filter(s => s.includes('fish')).length} fish, ${mineCells.length} mines`);
|
|
241
|
+
LOG.debug(`[fish] ${cellMap.join(' ')}`);
|
|
242
|
+
|
|
243
|
+
// Target the fish cell specifically for best catch rate
|
|
244
|
+
if (fishCell) {
|
|
245
|
+
target = { col: fishCell.col, row: fishCell.row };
|
|
246
|
+
LOG.info(`[fish] Fish detected at [${target.col},${target.row}]`);
|
|
247
|
+
} else {
|
|
248
|
+
// Fallback: pick a safe (non-mine) cell
|
|
249
|
+
const safe = allCells.filter(c => c.type !== 'mine');
|
|
250
|
+
target = safe.length > 0 ? { col: safe[0].col, row: safe[0].row } : null;
|
|
251
|
+
LOG.warn('[fish] No fish detected, picking safe cell');
|
|
252
|
+
}
|
|
253
|
+
} catch (e) {
|
|
254
|
+
LOG.error(`[fish] Image analysis failed: ${e.message}`);
|
|
255
|
+
// Fall back to clicking center cell
|
|
256
|
+
target = { col: 1, row: 1 };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!target) {
|
|
260
|
+
LOG.warn('[fish] No target cell found!');
|
|
261
|
+
return { outcome: 'no_target', header: 'no target', cd: null, clickedCount: 0 };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const catchBtns = getAllButtons(gameMsg).filter(b => (b.label || '').toLowerCase() === 'catch' && !b.disabled);
|
|
265
|
+
const btn = catchBtns.find(b => {
|
|
266
|
+
const m = (b.customId || '').match(/:(\d+):(\d+)$/);
|
|
267
|
+
return m && parseInt(m[1]) === target.col && parseInt(m[2]) === target.row;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (!btn) {
|
|
271
|
+
LOG.warn(`[fish] Catch button [${target.col},${target.row}] not found`);
|
|
272
|
+
return { outcome: 'no_button', header: 'button not found', cd: null, clickedCount: 0 };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
LOG.info(`[fish] Clicking safe cell [${target.col},${target.row}]`);
|
|
276
|
+
await humanDelay(80, 200);
|
|
277
|
+
try {
|
|
278
|
+
await safeClickButton(gameMsg, btn);
|
|
279
|
+
} catch (e) {
|
|
280
|
+
LOG.error(`[fish] Catch click error: ${e.message}`);
|
|
281
|
+
return { outcome: 'click_error', header: e.message, cd: null, clickedCount: 0 };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Wait for result screen
|
|
285
|
+
const resultMsg = await waitForResult({ channel, msgId, waitForDankMemer });
|
|
286
|
+
if (!resultMsg) return { outcome: 'no_result', header: 'no result', cd: null, clickedCount: 1 };
|
|
287
|
+
|
|
288
|
+
logMsg(resultMsg, 'fish-result');
|
|
289
|
+
const text = getFullText(resultMsg);
|
|
290
|
+
const result = parseFishResult(text);
|
|
291
|
+
result.clickedCount = 1;
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Main fish handler — single round (for grinder integration).
|
|
297
|
+
* @param {object} opts
|
|
298
|
+
* @param {object} opts.channel
|
|
299
|
+
* @param {function} opts.waitForDankMemer
|
|
300
|
+
* @returns {Promise<{result: string, coins: number, nextCooldownSec?: number}>}
|
|
301
|
+
*/
|
|
302
|
+
async function runFish({ channel, waitForDankMemer }) {
|
|
303
|
+
LOG.cmd(`${c.white}${c.bold}pls fish catch${c.reset}`);
|
|
304
|
+
|
|
305
|
+
await channel.send('pls fish catch');
|
|
306
|
+
let response = await waitForDankMemer(10000);
|
|
307
|
+
if (!response) return { result: 'no response', coins: 0 };
|
|
308
|
+
|
|
309
|
+
if (isHoldTight(response)) {
|
|
310
|
+
const reason = getHoldTightReason(response);
|
|
311
|
+
LOG.warn(`[fish] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
|
|
312
|
+
await sleep(30000);
|
|
313
|
+
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
logMsg(response, 'fish');
|
|
317
|
+
const text = getFullText(response);
|
|
318
|
+
|
|
319
|
+
// Check for cooldown
|
|
320
|
+
const cd = parseFishCooldown(text);
|
|
321
|
+
if (cd && (text.toLowerCase().includes('fish again') || text.toLowerCase().includes('can fish'))) {
|
|
322
|
+
LOG.info(`[fish] On cooldown — ${cd}s`);
|
|
323
|
+
return { result: `fish cooldown (${cd}s)`, coins: 0, nextCooldownSec: cd };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Re-fetch for hydrated CV2 components
|
|
327
|
+
const msgId = response.id;
|
|
328
|
+
const fresh = await refetchMsg(channel, msgId);
|
|
329
|
+
if (fresh) response = fresh;
|
|
330
|
+
|
|
331
|
+
// Find "Go Fishing" button
|
|
332
|
+
const goFishBtn = getAllButtons(response).find(b =>
|
|
333
|
+
!b.disabled && ((b.customId || '').includes('fish-catch-fish') ||
|
|
334
|
+
(b.label || '').toLowerCase().includes('go fishing'))
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
if (!goFishBtn) {
|
|
338
|
+
LOG.warn('[fish] No "Go Fishing" button');
|
|
339
|
+
return { result: text.substring(0, 60) || 'no button', coins: 0 };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Click "Go Fishing"
|
|
343
|
+
LOG.info('[fish] Clicking "Go Fishing"...');
|
|
344
|
+
await humanDelay();
|
|
345
|
+
try { await safeClickButton(response, goFishBtn); } catch (e) {
|
|
346
|
+
LOG.error(`[fish] Go Fishing click error: ${e.message}`);
|
|
347
|
+
return { result: 'click error', coins: 0 };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Wait for Catch grid OR direct result
|
|
351
|
+
const outcome = await waitForFishingOutcome({ channel, msgId, waitForDankMemer });
|
|
352
|
+
if (!outcome) {
|
|
353
|
+
LOG.warn('[fish] No fishing outcome');
|
|
354
|
+
return { result: 'no outcome', coins: 0 };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let roundResult;
|
|
358
|
+
if (outcome.type === 'grid') {
|
|
359
|
+
roundResult = await playFishRound({ gameMsg: outcome.msg, channel, msgId, waitForDankMemer });
|
|
360
|
+
} else {
|
|
361
|
+
logMsg(outcome.msg, 'fish-result');
|
|
362
|
+
const resultText = getFullText(outcome.msg);
|
|
363
|
+
roundResult = parseFishResult(resultText);
|
|
364
|
+
roundResult.clickedCount = 0;
|
|
365
|
+
}
|
|
366
|
+
return formatRoundResult(roundResult);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Fish-only grinding loop — keeps fishing without re-sending the command.
|
|
371
|
+
* Clicks "Fish Again" after each round. Stops when bucket is full or cooldown.
|
|
372
|
+
* @param {object} opts
|
|
373
|
+
* @param {object} opts.channel
|
|
374
|
+
* @param {function} opts.waitForDankMemer
|
|
375
|
+
* @param {number} [opts.maxRounds=50] - Max rounds per session
|
|
376
|
+
* @param {function} [opts.onRound] - Callback per round: (roundNum, result) => void
|
|
377
|
+
* @param {AbortSignal} [opts.signal] - Signal to stop early
|
|
378
|
+
* @returns {Promise<{totalRounds, totalCaught, totalMines, results: Array}>}
|
|
379
|
+
*/
|
|
380
|
+
async function runFishLoop({ channel, waitForDankMemer, maxRounds = 50, onRound, signal }) {
|
|
381
|
+
LOG.cmd(`${c.white}${c.bold}pls fish catch${c.reset} (loop mode, max ${maxRounds} rounds)`);
|
|
382
|
+
|
|
383
|
+
await channel.send('pls fish catch');
|
|
384
|
+
let response = await waitForDankMemer(10000);
|
|
385
|
+
if (!response) return { totalRounds: 0, totalCaught: 0, totalMines: 0, results: [] };
|
|
386
|
+
|
|
387
|
+
if (isHoldTight(response)) {
|
|
388
|
+
const reason = getHoldTightReason(response);
|
|
389
|
+
LOG.warn(`[fish] Hold Tight — waiting 30s`);
|
|
390
|
+
await sleep(30000);
|
|
391
|
+
return { totalRounds: 0, totalCaught: 0, totalMines: 0, results: [] };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Refetch for hydrated components
|
|
395
|
+
let msgId = response.id;
|
|
396
|
+
const fresh = await refetchMsg(channel, msgId);
|
|
397
|
+
if (fresh) response = fresh;
|
|
398
|
+
|
|
399
|
+
// Check cooldown — wait for it instead of returning
|
|
400
|
+
const text = getFullText(response);
|
|
401
|
+
const cd = parseFishCooldown(text);
|
|
402
|
+
if (cd && cd > 0) {
|
|
403
|
+
LOG.info(`[fish] On cooldown — ${cd}s, waiting...`);
|
|
404
|
+
await sleep(cd * 1000 + 1000);
|
|
405
|
+
response = await refetchMsg(channel, msgId);
|
|
406
|
+
if (!response) return { totalRounds: 0, totalCaught: 0, totalMines: 0, results: [] };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Click "Go Fishing" to start
|
|
410
|
+
const goFishBtn = getAllButtons(response).find(b =>
|
|
411
|
+
!b.disabled && ((b.customId || '').includes('fish-catch-fish') ||
|
|
412
|
+
(b.label || '').toLowerCase().includes('go fishing'))
|
|
413
|
+
);
|
|
414
|
+
if (!goFishBtn) {
|
|
415
|
+
LOG.warn('[fish] No Go Fishing button on dashboard');
|
|
416
|
+
return { totalRounds: 0, totalCaught: 0, totalMines: 0, results: [] };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Sell all fish from buckets before starting to ensure space
|
|
420
|
+
LOG.info('[fish] Clearing bucket before starting...');
|
|
421
|
+
await sellAllFish({ channel, waitForDankMemer });
|
|
422
|
+
|
|
423
|
+
// Re-send command to get fresh dashboard after sell
|
|
424
|
+
await channel.send('pls fish catch');
|
|
425
|
+
let newDash = await waitForDankMemer(8000);
|
|
426
|
+
if (newDash) {
|
|
427
|
+
let freshDash = await refetchMsg(channel, newDash.id);
|
|
428
|
+
if (freshDash) newDash = freshDash;
|
|
429
|
+
msgId = newDash.id;
|
|
430
|
+
response = newDash;
|
|
431
|
+
|
|
432
|
+
// Wait for any cooldown on fresh dashboard
|
|
433
|
+
const newCd = parseFishCooldown(getFullText(response));
|
|
434
|
+
if (newCd && newCd > 0) {
|
|
435
|
+
LOG.info(`[fish] Cooldown after sell — ${newCd}s, waiting...`);
|
|
436
|
+
await sleep(newCd * 1000 + 1000);
|
|
437
|
+
response = await refetchMsg(channel, msgId);
|
|
438
|
+
if (!response) return { totalRounds: 0, totalCaught: 0, totalMines: 0, results: [] };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
LOG.info('[fish] Starting fishing loop...');
|
|
443
|
+
await humanDelay();
|
|
444
|
+
const goFishBtn2 = getAllButtons(response).find(b =>
|
|
445
|
+
!b.disabled && ((b.customId || '').includes('fish-catch-fish') ||
|
|
446
|
+
(b.label || '').toLowerCase().includes('go fishing'))
|
|
447
|
+
);
|
|
448
|
+
if (!goFishBtn2) {
|
|
449
|
+
LOG.warn('[fish] No Go Fishing button after sell');
|
|
450
|
+
return { totalRounds: 0, totalCaught: 0, totalMines: 0, results: [] };
|
|
451
|
+
}
|
|
452
|
+
try { await safeClickButton(response, goFishBtn2); } catch (e) {
|
|
453
|
+
LOG.error(`[fish] Go Fishing click error: ${e.message}`);
|
|
454
|
+
return { totalRounds: 0, totalCaught: 0, totalMines: 0, results: [] };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const results = [];
|
|
458
|
+
let totalCaught = 0, totalMines = 0;
|
|
459
|
+
|
|
460
|
+
for (let round = 0; round < maxRounds; round++) {
|
|
461
|
+
if (signal?.aborted) break;
|
|
462
|
+
|
|
463
|
+
// Wait for Catch grid OR direct result (e.g. "nothing to catch")
|
|
464
|
+
const outcome = await waitForFishingOutcome({ channel, msgId, waitForDankMemer });
|
|
465
|
+
if (!outcome) {
|
|
466
|
+
LOG.warn(`[fish] Round ${round + 1}: no fishing outcome`);
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
let roundResult;
|
|
471
|
+
if (outcome.type === 'grid') {
|
|
472
|
+
// Normal: grid appeared, play the round
|
|
473
|
+
roundResult = await playFishRound({ gameMsg: outcome.msg, channel, msgId, waitForDankMemer });
|
|
474
|
+
} else {
|
|
475
|
+
// Direct result without grid (e.g. "nothing to catch", instant mine)
|
|
476
|
+
logMsg(outcome.msg, 'fish-result');
|
|
477
|
+
const text = getFullText(outcome.msg);
|
|
478
|
+
roundResult = parseFishResult(text);
|
|
479
|
+
roundResult.clickedCount = 0;
|
|
480
|
+
LOG.debug(`[fish] Direct result: ${roundResult.outcome}`);
|
|
481
|
+
}
|
|
482
|
+
results.push(roundResult);
|
|
483
|
+
|
|
484
|
+
if (roundResult.outcome === 'caught') totalCaught++;
|
|
485
|
+
if (roundResult.outcome === 'mine') totalMines++;
|
|
486
|
+
|
|
487
|
+
const formatted = formatRoundResult(roundResult);
|
|
488
|
+
LOG.info(`[fish] Round ${round + 1}: ${formatted.result}`);
|
|
489
|
+
if (onRound) onRound(round + 1, roundResult);
|
|
490
|
+
|
|
491
|
+
// Result screen has: Go Back, Open Buckets, Sell Creature (coins/tokens),
|
|
492
|
+
// and "Fish Again" (SECTION accessory button).
|
|
493
|
+
// NOTE: Sell Creature from result screen triggers EPHEMERAL confirmation
|
|
494
|
+
// which selfbots can't interact with. Use "pls fish buckets" → "Sell All Fish"
|
|
495
|
+
// instead (every N rounds or when bucket is full).
|
|
496
|
+
|
|
497
|
+
await sleep(500);
|
|
498
|
+
|
|
499
|
+
// Check if bucket is full — sell all from buckets to free space
|
|
500
|
+
const resultMsg = await refetchMsg(channel, msgId);
|
|
501
|
+
if (resultMsg) {
|
|
502
|
+
const resultText = getFullText(resultMsg).toLowerCase();
|
|
503
|
+
if (resultText.includes('no more bucket space')) {
|
|
504
|
+
LOG.warn('[fish] Bucket full — selling all from buckets');
|
|
505
|
+
await sellAllFish({ channel, waitForDankMemer });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Wait for cooldown
|
|
510
|
+
if (roundResult.cd && roundResult.cd > 0) {
|
|
511
|
+
LOG.info(`[fish] Cooldown — ${roundResult.cd}s, waiting...`);
|
|
512
|
+
await sleep(roundResult.cd * 1000 + 1000);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Click "Fish Again" — try multiple approaches
|
|
516
|
+
let fishAgainClicked = false;
|
|
517
|
+
|
|
518
|
+
// Drain stale events accumulated during cooldown/sell wait
|
|
519
|
+
for (let d = 0; d < 10; d++) {
|
|
520
|
+
const stale = await waitForDankMemer(300);
|
|
521
|
+
if (!stale) break;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Approach 1: Refetch the catch result message and click Fish Again
|
|
525
|
+
for (let retry = 0; retry < 3 && !fishAgainClicked; retry++) {
|
|
526
|
+
const currentMsg = await refetchMsg(channel, msgId);
|
|
527
|
+
if (!currentMsg) break;
|
|
528
|
+
const fishAgainBtn = getAllButtons(currentMsg).find(b =>
|
|
529
|
+
!b.disabled && (b.customId || '').includes('fish-catch-fish')
|
|
530
|
+
);
|
|
531
|
+
if (fishAgainBtn) {
|
|
532
|
+
await humanDelay(80, 200);
|
|
533
|
+
try {
|
|
534
|
+
await safeClickButton(currentMsg, fishAgainBtn);
|
|
535
|
+
fishAgainClicked = true;
|
|
536
|
+
} catch (e) {
|
|
537
|
+
LOG.debug(`[fish] Fish Again attempt ${retry + 1} error: ${e.message}`);
|
|
538
|
+
await sleep(2000);
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
// Maybe still on CD — check and wait
|
|
542
|
+
const txt = getFullText(currentMsg);
|
|
543
|
+
const remainCd = parseFishCooldown(txt);
|
|
544
|
+
if (remainCd && remainCd > 0) {
|
|
545
|
+
LOG.debug(`[fish] Still on CD (${remainCd}s), waiting...`);
|
|
546
|
+
await sleep(remainCd * 1000 + 1000);
|
|
547
|
+
} else {
|
|
548
|
+
await sleep(2000);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Approach 2: If Fish Again not found, re-send "pls fish catch" fresh
|
|
554
|
+
if (!fishAgainClicked) {
|
|
555
|
+
LOG.debug('[fish] Fish Again not found, re-sending pls fish catch');
|
|
556
|
+
await channel.send('pls fish catch');
|
|
557
|
+
let newResp = await waitForDankMemer(8000);
|
|
558
|
+
if (!newResp) { LOG.warn('[fish] No response to re-send'); break; }
|
|
559
|
+
let freshResp = await refetchMsg(channel, newResp.id);
|
|
560
|
+
if (freshResp) newResp = freshResp;
|
|
561
|
+
msgId = newResp.id;
|
|
562
|
+
|
|
563
|
+
// Wait for any remaining cooldown
|
|
564
|
+
const txt = getFullText(newResp);
|
|
565
|
+
const remainCd = parseFishCooldown(txt);
|
|
566
|
+
if (remainCd && remainCd > 0) {
|
|
567
|
+
await sleep(remainCd * 1000 + 1000);
|
|
568
|
+
newResp = await refetchMsg(channel, msgId);
|
|
569
|
+
if (!newResp) break;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const goFishBtn3 = getAllButtons(newResp).find(b =>
|
|
573
|
+
!b.disabled && (b.customId || '').includes('fish-catch-fish')
|
|
574
|
+
);
|
|
575
|
+
if (goFishBtn3) {
|
|
576
|
+
await humanDelay(80, 200);
|
|
577
|
+
try {
|
|
578
|
+
await safeClickButton(newResp, goFishBtn3);
|
|
579
|
+
fishAgainClicked = true;
|
|
580
|
+
} catch (e) {
|
|
581
|
+
LOG.debug(`[fish] Go Fishing re-send error: ${e.message}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (!fishAgainClicked) {
|
|
587
|
+
LOG.info('[fish] Could not start next round — stopping loop');
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
LOG.success(`[fish] Fishing loop done: ${results.length} rounds, ${totalCaught} caught, ${totalMines} mines`);
|
|
593
|
+
return { totalRounds: results.length, totalCaught, totalMines, results };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Format a round result into {result, coins, nextCooldownSec}.
|
|
598
|
+
*/
|
|
599
|
+
function formatRoundResult(r) {
|
|
600
|
+
const coins = 0; // Fish don't give coins directly; value is from selling
|
|
601
|
+
switch (r.outcome) {
|
|
602
|
+
case 'caught':
|
|
603
|
+
return { result: `fish → caught ${r.fish || 'a fish'}`, coins, nextCooldownSec: r.cd };
|
|
604
|
+
case 'mine':
|
|
605
|
+
return { result: 'fish → triggered mine', coins, nextCooldownSec: r.cd };
|
|
606
|
+
case 'nothing':
|
|
607
|
+
return { result: 'fish → nothing', coins, nextCooldownSec: r.cd };
|
|
608
|
+
case 'got_away':
|
|
609
|
+
return { result: 'fish → got away', coins, nextCooldownSec: r.cd };
|
|
610
|
+
default:
|
|
611
|
+
return { result: `fish → ${r.header || r.outcome}`, coins, nextCooldownSec: r.cd };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
module.exports = { runFish, runFishLoop, sellAllFish };
|