dankgrinder 3.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 +916 -358
- package/package.json +5 -3
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fish Vision Module — analyze fishing minigame grid images.
|
|
3
|
+
* Detects mines (dark patches) vs safe/fish cells using pixel analysis.
|
|
4
|
+
*
|
|
5
|
+
* Mine cells have significantly higher percentage of dark pixels (< 60 brightness).
|
|
6
|
+
* Safe cells: dark% ≈ 0, avgBrightness ≈ 83
|
|
7
|
+
* Mine cells: dark% > 8, avgBrightness < 80
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const sharp = require('sharp');
|
|
11
|
+
const https = require('https');
|
|
12
|
+
const http = require('http');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Download an image from a URL and return as Buffer.
|
|
16
|
+
*/
|
|
17
|
+
function downloadImage(url) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const proto = url.startsWith('https') ? https : http;
|
|
20
|
+
const req = proto.get(url, res => {
|
|
21
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
22
|
+
return downloadImage(res.headers.location).then(resolve, reject);
|
|
23
|
+
}
|
|
24
|
+
const chunks = [];
|
|
25
|
+
res.on('data', c => chunks.push(c));
|
|
26
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
27
|
+
res.on('error', reject);
|
|
28
|
+
});
|
|
29
|
+
req.on('error', reject);
|
|
30
|
+
req.setTimeout(10000, () => { req.destroy(); reject(new Error('download timeout')); });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract image URL from MEDIA_GALLERY inside CV2 components.
|
|
36
|
+
*/
|
|
37
|
+
function extractImageUrl(components) {
|
|
38
|
+
for (const item of components || []) {
|
|
39
|
+
if (item.data?.items) {
|
|
40
|
+
for (const g of item.data.items) {
|
|
41
|
+
if (g.media?.url) return g.media.url;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (item.components) {
|
|
45
|
+
const found = extractImageUrl(item.components);
|
|
46
|
+
if (found) return found;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Analyze a fishing grid image — classify each cell as empty, fish, or mine.
|
|
54
|
+
*
|
|
55
|
+
* Grid layout: 3x3 = 9 cells. Each round has exactly:
|
|
56
|
+
* - 1 fish cell (dark silhouette, higher fill ratio ~0.32, darkPct ~10)
|
|
57
|
+
* - 2 mine cells (dark silhouette, lower fill ratio ~0.15, darkPct ~6)
|
|
58
|
+
* - 6 empty cells (uniform blue water, darkPct ≈ 0)
|
|
59
|
+
*
|
|
60
|
+
* Classification features (from empirical data across many rounds):
|
|
61
|
+
* FISH: darkPct≥8, fillRatio>0.25, aspect<1, spread<0.7
|
|
62
|
+
* MINE: darkPct≥4, fillRatio<0.2, aspect≥1, spread≥0.9
|
|
63
|
+
* EMPTY: darkPct<4
|
|
64
|
+
*
|
|
65
|
+
* @param {Buffer} imgBuffer - The image data
|
|
66
|
+
* @param {number} gridCols - Number of columns (default 3)
|
|
67
|
+
* @param {number} gridRows - Number of rows (default 3)
|
|
68
|
+
* @returns {Promise<Array<{col, row, type, darkPct, fillRatio}>>}
|
|
69
|
+
*/
|
|
70
|
+
async function analyzeGrid(imgBuffer, gridCols = 3, gridRows = 3) {
|
|
71
|
+
const { data, info } = await sharp(imgBuffer).raw().toBuffer({ resolveWithObject: true });
|
|
72
|
+
const { width, height, channels } = info;
|
|
73
|
+
const cellW = Math.floor(width / gridCols);
|
|
74
|
+
const cellH = Math.floor(height / gridRows);
|
|
75
|
+
|
|
76
|
+
const cells = [];
|
|
77
|
+
for (let row = 0; row < gridRows; row++) {
|
|
78
|
+
for (let col = 0; col < gridCols; col++) {
|
|
79
|
+
const startX = col * cellW, startY = row * cellH;
|
|
80
|
+
const endX = Math.min(startX + cellW, width);
|
|
81
|
+
const endY = Math.min(startY + cellH, height);
|
|
82
|
+
|
|
83
|
+
// Collect dark pixels (brightness < 50)
|
|
84
|
+
const darkPixels = [];
|
|
85
|
+
let totalPixels = 0;
|
|
86
|
+
for (let y = startY; y < endY; y++) {
|
|
87
|
+
for (let x = startX; x < endX; x++) {
|
|
88
|
+
totalPixels++;
|
|
89
|
+
const idx = (y * width + x) * channels;
|
|
90
|
+
const r = data[idx], g = data[idx + 1], b = data[idx + 2];
|
|
91
|
+
if ((r + g + b) / 3 < 50) darkPixels.push({ x: x - startX, y: y - startY });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const darkPct = Math.round(darkPixels.length / totalPixels * 100);
|
|
96
|
+
|
|
97
|
+
if (darkPixels.length < 10 || darkPct < 4) {
|
|
98
|
+
cells.push({ col, row, type: 'empty', darkPct, fillRatio: 0 });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Compute bounding box and fill ratio
|
|
103
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
104
|
+
for (const p of darkPixels) {
|
|
105
|
+
minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x);
|
|
106
|
+
minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y);
|
|
107
|
+
}
|
|
108
|
+
const bboxW = maxX - minX + 1, bboxH = maxY - minY + 1;
|
|
109
|
+
const fillRatio = darkPixels.length / (bboxW * bboxH);
|
|
110
|
+
|
|
111
|
+
// Classify: fish has higher fill ratio (~0.32) vs mine (~0.15)
|
|
112
|
+
let type;
|
|
113
|
+
if (fillRatio > 0.22) {
|
|
114
|
+
type = 'fish';
|
|
115
|
+
} else {
|
|
116
|
+
type = 'mine';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
cells.push({ col, row, type, darkPct, fillRatio: +fillRatio.toFixed(3) });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return cells;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Find fish cell and mine cells from a fishing grid image.
|
|
128
|
+
* @param {Buffer} imgBuffer
|
|
129
|
+
* @returns {Promise<{fishCell: {col, row}|null, mineCells: Array<{col, row}>, emptyCells: Array<{col, row}>, allCells: Array}>}
|
|
130
|
+
*/
|
|
131
|
+
async function findSafeCells(imgBuffer) {
|
|
132
|
+
const allCells = await analyzeGrid(imgBuffer);
|
|
133
|
+
const fishCell = allCells.find(c => c.type === 'fish') || null;
|
|
134
|
+
const mineCells = allCells.filter(c => c.type === 'mine').map(c => ({ col: c.col, row: c.row }));
|
|
135
|
+
const emptyCells = allCells.filter(c => c.type === 'empty').map(c => ({ col: c.col, row: c.row }));
|
|
136
|
+
// safeCells = fish + empty (everything except mines)
|
|
137
|
+
const safeCells = allCells.filter(c => c.type !== 'mine').map(c => ({ col: c.col, row: c.row }));
|
|
138
|
+
return { fishCell, mineCells, emptyCells, safeCells, allCells };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { downloadImage, extractImageUrl, analyzeGrid, findSafeCells };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple gambling command handlers.
|
|
3
|
+
* Covers: coinflip, roulette, slots, snakeeyes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
|
|
8
|
+
logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
|
|
9
|
+
} = require('./utils');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generic gamble handler — works for coinflip, roulette, slots, snakeeyes.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} opts
|
|
15
|
+
* @param {object} opts.channel
|
|
16
|
+
* @param {function} opts.waitForDankMemer
|
|
17
|
+
* @param {string} opts.cmdName - e.g. 'coinflip', 'roulette', 'slots', 'snakeeyes'
|
|
18
|
+
* @param {string} opts.cmdString - Full command string e.g. 'pls coinflip 1000 heads'
|
|
19
|
+
* @returns {Promise<{result: string, coins: number}>}
|
|
20
|
+
*/
|
|
21
|
+
async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
|
|
22
|
+
LOG.cmd(`${c.white}${c.bold}${cmdString}${c.reset}`);
|
|
23
|
+
|
|
24
|
+
await channel.send(cmdString);
|
|
25
|
+
const response = await waitForDankMemer(10000);
|
|
26
|
+
|
|
27
|
+
if (!response) {
|
|
28
|
+
LOG.warn(`[${cmdName}] No response`);
|
|
29
|
+
return { result: 'no response', coins: 0 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (isHoldTight(response)) {
|
|
33
|
+
const reason = getHoldTightReason(response);
|
|
34
|
+
LOG.warn(`[${cmdName}] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
|
|
35
|
+
await sleep(30000);
|
|
36
|
+
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
logMsg(response, cmdName);
|
|
40
|
+
const text = getFullText(response);
|
|
41
|
+
const coins = parseCoins(text);
|
|
42
|
+
|
|
43
|
+
// Some gambles have buttons (e.g. pick heads/tails)
|
|
44
|
+
const buttons = getAllButtons(response);
|
|
45
|
+
if (buttons.length > 0) {
|
|
46
|
+
const btn = buttons.find(b => !b.disabled);
|
|
47
|
+
if (btn) {
|
|
48
|
+
LOG.info(`[${cmdName}] Clicking "${btn.label}"`);
|
|
49
|
+
await humanDelay();
|
|
50
|
+
try {
|
|
51
|
+
const followUp = await safeClickButton(response, btn);
|
|
52
|
+
if (followUp) {
|
|
53
|
+
logMsg(followUp, `${cmdName}-result`);
|
|
54
|
+
const fText = getFullText(followUp);
|
|
55
|
+
const fCoins = parseCoins(fText);
|
|
56
|
+
if (fCoins > 0) {
|
|
57
|
+
LOG.coin(`[${cmdName}] ${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`);
|
|
58
|
+
return { result: `${cmdName} → +⏣ ${fCoins.toLocaleString()}`, coins: fCoins };
|
|
59
|
+
}
|
|
60
|
+
if (fText.toLowerCase().includes('won')) return { result: `${cmdName} → ${c.green}won${c.reset}`, coins: 0 };
|
|
61
|
+
if (fText.toLowerCase().includes('lost')) return { result: `${cmdName} → ${c.red}lost${c.reset}`, coins: 0 };
|
|
62
|
+
}
|
|
63
|
+
} catch (e) { LOG.error(`[${cmdName}] Click error: ${e.message}`); }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (coins > 0) {
|
|
68
|
+
LOG.coin(`[${cmdName}] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
69
|
+
return { result: `${cmdName} → +⏣ ${coins.toLocaleString()}`, coins };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const lower = text.toLowerCase();
|
|
73
|
+
if (lower.includes('won')) return { result: `${cmdName} → ${c.green}won${c.reset}`, coins: 0 };
|
|
74
|
+
if (lower.includes('lost')) return { result: `${cmdName} → ${c.red}lost${c.reset}`, coins: 0 };
|
|
75
|
+
|
|
76
|
+
return { result: `${cmdName} done`, coins: 0 };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Convenience wrappers
|
|
80
|
+
async function runCoinflip({ channel, waitForDankMemer, betAmount = 1000 }) {
|
|
81
|
+
return runGamble({ channel, waitForDankMemer, cmdName: 'coinflip', cmdString: `pls coinflip ${betAmount} heads` });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function runRoulette({ channel, waitForDankMemer, betAmount = 1000 }) {
|
|
85
|
+
return runGamble({ channel, waitForDankMemer, cmdName: 'roulette', cmdString: `pls roulette ${betAmount} red` });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function runSlots({ channel, waitForDankMemer, betAmount = 1000 }) {
|
|
89
|
+
return runGamble({ channel, waitForDankMemer, cmdName: 'slots', cmdString: `pls slots ${betAmount}` });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 1000 }) {
|
|
93
|
+
return runGamble({ channel, waitForDankMemer, cmdName: 'snakeeyes', cmdString: `pls snakeeyes ${betAmount}` });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { runGamble, runCoinflip, runRoulette, runSlots, runSnakeeyes };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic command handler.
|
|
3
|
+
* Handles simple commands like: beg, farm, tidy, daily, weekly, monthly, stream, drops, use, alert.
|
|
4
|
+
* Detects missing items and auto-buys. Handles buttons and select menus.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
LOG, c, getFullText, parseCoins, getAllButtons, getAllSelectMenus,
|
|
9
|
+
safeClickButton, logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, needsItem,
|
|
10
|
+
} = require('./utils');
|
|
11
|
+
const { buyItem } = require('./shop');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} opts
|
|
15
|
+
* @param {object} opts.channel
|
|
16
|
+
* @param {function} opts.waitForDankMemer
|
|
17
|
+
* @param {string} opts.cmdString - Full command string e.g. 'pls farm'
|
|
18
|
+
* @param {string} opts.cmdName - Short name e.g. 'farm'
|
|
19
|
+
* @param {object} [opts.client]
|
|
20
|
+
* @returns {Promise<{result: string, coins: number}>}
|
|
21
|
+
*/
|
|
22
|
+
async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, client }) {
|
|
23
|
+
LOG.cmd(`${c.white}${c.bold}${cmdString}${c.reset}`);
|
|
24
|
+
|
|
25
|
+
await channel.send(cmdString);
|
|
26
|
+
const response = await waitForDankMemer(10000);
|
|
27
|
+
|
|
28
|
+
if (!response) {
|
|
29
|
+
LOG.warn(`[${cmdName}] No response`);
|
|
30
|
+
return { result: 'no response', coins: 0 };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (isHoldTight(response)) {
|
|
34
|
+
const reason = getHoldTightReason(response);
|
|
35
|
+
LOG.warn(`[${cmdName}] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
|
|
36
|
+
await sleep(30000);
|
|
37
|
+
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
logMsg(response, cmdName);
|
|
41
|
+
const text = getFullText(response);
|
|
42
|
+
const coins = parseCoins(text);
|
|
43
|
+
|
|
44
|
+
// Check if we need an item
|
|
45
|
+
const missing = needsItem(text);
|
|
46
|
+
if (missing) {
|
|
47
|
+
LOG.warn(`[${cmdName}] Missing ${c.bold}${missing}${c.reset} — auto-buying...`);
|
|
48
|
+
const bought = await buyItem({ channel, waitForDankMemer, client, itemName: missing, quantity: 3 });
|
|
49
|
+
if (bought) {
|
|
50
|
+
LOG.success(`[${cmdName}] Bought ${missing}, retrying command...`);
|
|
51
|
+
await sleep(3000);
|
|
52
|
+
await channel.send(cmdString);
|
|
53
|
+
const r2 = await waitForDankMemer(10000);
|
|
54
|
+
if (r2) {
|
|
55
|
+
logMsg(r2, `${cmdName}-retry`);
|
|
56
|
+
const t2 = getFullText(r2);
|
|
57
|
+
const c2 = parseCoins(t2);
|
|
58
|
+
if (c2 > 0) {
|
|
59
|
+
LOG.coin(`[${cmdName}] ${c.green}+⏣ ${c2.toLocaleString()}${c.reset}`);
|
|
60
|
+
return { result: `auto-bought ${missing} → +⏣ ${c2.toLocaleString()}`, coins: c2 };
|
|
61
|
+
}
|
|
62
|
+
return { result: `auto-bought ${missing}`, coins: 0 };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { result: `need ${missing} (buy failed)`, coins: 0 };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Handle buttons if present
|
|
69
|
+
const buttons = getAllButtons(response);
|
|
70
|
+
if (buttons.length > 0) {
|
|
71
|
+
const btn = buttons.find(b => !b.disabled) || buttons[0];
|
|
72
|
+
if (btn && !btn.disabled) {
|
|
73
|
+
LOG.info(`[${cmdName}] Clicking "${btn.label || '?'}"`);
|
|
74
|
+
await humanDelay();
|
|
75
|
+
try {
|
|
76
|
+
const followUp = await safeClickButton(response, btn);
|
|
77
|
+
if (followUp) {
|
|
78
|
+
logMsg(followUp, `${cmdName}-followup`);
|
|
79
|
+
const fText = getFullText(followUp);
|
|
80
|
+
const fCoins = parseCoins(fText);
|
|
81
|
+
if (fCoins > 0) {
|
|
82
|
+
LOG.coin(`[${cmdName}] ${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`);
|
|
83
|
+
return { result: `+⏣ ${fCoins.toLocaleString()}`, coins: fCoins };
|
|
84
|
+
}
|
|
85
|
+
// Multi-step: click next button too
|
|
86
|
+
const nextButtons = getAllButtons(followUp);
|
|
87
|
+
if (nextButtons.length > 0) {
|
|
88
|
+
const nextBtn = nextButtons.find(b => !b.disabled);
|
|
89
|
+
if (nextBtn) {
|
|
90
|
+
await humanDelay();
|
|
91
|
+
try { await safeClickButton(followUp, nextBtn); } catch {}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch (e) { LOG.error(`[${cmdName}] Click error: ${e.message}`); }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Handle select menus
|
|
100
|
+
const menus = getAllSelectMenus(response);
|
|
101
|
+
if (menus.length > 0) {
|
|
102
|
+
// Re-fetch for hydrated components (minValues/maxValues)
|
|
103
|
+
const freshMsg = await channel.messages.fetch(response.id).catch(() => null);
|
|
104
|
+
if (freshMsg) response = freshMsg;
|
|
105
|
+
// Find row index of first select menu
|
|
106
|
+
let menuRowIdx = -1;
|
|
107
|
+
for (let i = 0; i < (response.components || []).length; i++) {
|
|
108
|
+
for (const comp of response.components[i].components || []) {
|
|
109
|
+
if (comp.type === 'STRING_SELECT' || comp.type === 3) { menuRowIdx = i; break; }
|
|
110
|
+
}
|
|
111
|
+
if (menuRowIdx >= 0) break;
|
|
112
|
+
}
|
|
113
|
+
const menu = menuRowIdx >= 0 ? response.components[menuRowIdx].components[0] : null;
|
|
114
|
+
const options = menu?.options || [];
|
|
115
|
+
if (options.length > 0 && menuRowIdx >= 0) {
|
|
116
|
+
const opt = options[Math.floor(Math.random() * options.length)];
|
|
117
|
+
LOG.info(`[${cmdName}] Selecting: "${opt.label}"`);
|
|
118
|
+
try {
|
|
119
|
+
await response.selectMenu(menuRowIdx, [opt.value]);
|
|
120
|
+
const followUp = await waitForDankMemer(8000);
|
|
121
|
+
if (followUp) {
|
|
122
|
+
const fText = getFullText(followUp);
|
|
123
|
+
const fCoins = parseCoins(fText);
|
|
124
|
+
if (fCoins > 0) {
|
|
125
|
+
LOG.coin(`[${cmdName}] ${opt.label} → ${c.green}+⏣ ${fCoins.toLocaleString()}${c.reset}`);
|
|
126
|
+
return { result: `${opt.label} → +⏣ ${fCoins.toLocaleString()}`, coins: fCoins };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch (e) { LOG.error(`[${cmdName}] Select error: ${e.message}`); }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (coins > 0) {
|
|
134
|
+
LOG.coin(`[${cmdName}] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
135
|
+
return { result: `+⏣ ${coins.toLocaleString()}`, coins };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { result: text.substring(0, 60) || 'done', coins: 0 };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Alert handler — click dismiss/accept/ok button.
|
|
143
|
+
*/
|
|
144
|
+
async function runAlert({ channel, waitForDankMemer }) {
|
|
145
|
+
LOG.cmd(`${c.white}${c.bold}pls alert${c.reset}`);
|
|
146
|
+
|
|
147
|
+
await channel.send('pls alert');
|
|
148
|
+
const response = await waitForDankMemer(10000);
|
|
149
|
+
|
|
150
|
+
if (!response) return { result: 'no response', coins: 0 };
|
|
151
|
+
if (isHoldTight(response)) {
|
|
152
|
+
const reason = getHoldTightReason(response);
|
|
153
|
+
LOG.warn(`[alert] Hold Tight${reason ? ` (reason: /${reason})` : ''}`);
|
|
154
|
+
await sleep(30000);
|
|
155
|
+
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
logMsg(response, 'alert');
|
|
159
|
+
const buttons = getAllButtons(response);
|
|
160
|
+
const dismissLabels = ['ok', 'dismiss', 'accept', 'got it', 'continue'];
|
|
161
|
+
const btn = buttons.find(b => !b.disabled && dismissLabels.some(s => (b.label || '').toLowerCase().includes(s)));
|
|
162
|
+
|
|
163
|
+
if (btn) {
|
|
164
|
+
await humanDelay();
|
|
165
|
+
try { await safeClickButton(response, btn); } catch {}
|
|
166
|
+
LOG.info('[alert] Dismissed');
|
|
167
|
+
return { result: 'alert dismissed', coins: 0 };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (buttons.length > 0) {
|
|
171
|
+
const first = buttons.find(b => !b.disabled);
|
|
172
|
+
if (first) {
|
|
173
|
+
await humanDelay();
|
|
174
|
+
try { await safeClickButton(response, first); } catch {}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { result: 'alert handled', coins: 0 };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = { runGeneric, runAlert };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HighLow command handler.
|
|
3
|
+
* Send "pls hl", parse the hint number, pick higher/lower/jackpot.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
|
|
8
|
+
logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
|
|
9
|
+
} = require('./utils');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Recursively handle highlow rounds (multi-round game).
|
|
13
|
+
*/
|
|
14
|
+
async function playHighLow(response, depth = 0) {
|
|
15
|
+
if (!response || depth > 5) return { result: 'done', coins: 0 };
|
|
16
|
+
|
|
17
|
+
const text = getFullText(response);
|
|
18
|
+
const buttons = getAllButtons(response);
|
|
19
|
+
|
|
20
|
+
if (buttons.length === 0) {
|
|
21
|
+
const coins = parseCoins(text);
|
|
22
|
+
return { result: coins > 0 ? `+⏣ ${coins.toLocaleString()}` : 'done', coins };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Parse the hint number
|
|
26
|
+
const match = text.match(/number.*?(\d+)/i) || text.match(/(\d+)/);
|
|
27
|
+
let targetBtn;
|
|
28
|
+
|
|
29
|
+
if (match && buttons.length >= 2) {
|
|
30
|
+
const num = parseInt(match[1]);
|
|
31
|
+
if (num > 50) {
|
|
32
|
+
targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('lower')) || buttons[1];
|
|
33
|
+
} else if (num < 50) {
|
|
34
|
+
targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('higher')) || buttons[0];
|
|
35
|
+
} else {
|
|
36
|
+
const jackpot = buttons.find(b => (b.label || '').toLowerCase().includes('jackpot'));
|
|
37
|
+
targetBtn = jackpot || buttons[Math.floor(Math.random() * 2)];
|
|
38
|
+
}
|
|
39
|
+
LOG.info(`[hl] Number: ${num} → picking "${targetBtn.label}"`);
|
|
40
|
+
} else {
|
|
41
|
+
targetBtn = buttons.find(b => !b.disabled) || buttons[0];
|
|
42
|
+
LOG.info(`[hl] No hint, picking "${targetBtn.label}"`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!targetBtn || targetBtn.disabled) {
|
|
46
|
+
const coins = parseCoins(text);
|
|
47
|
+
return { result: 'buttons disabled', coins };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await humanDelay(500, 1200);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const followUp = await safeClickButton(response, targetBtn);
|
|
54
|
+
if (followUp) {
|
|
55
|
+
logMsg(followUp, `hl-round-${depth}`);
|
|
56
|
+
const fText = getFullText(followUp);
|
|
57
|
+
const fCoins = parseCoins(fText);
|
|
58
|
+
|
|
59
|
+
// Check for more rounds
|
|
60
|
+
const moreButtons = getAllButtons(followUp);
|
|
61
|
+
if (moreButtons.length >= 2 && !moreButtons.every(b => b.disabled)) {
|
|
62
|
+
const deeper = await playHighLow(followUp, depth + 1);
|
|
63
|
+
return { result: deeper.result, coins: fCoins + deeper.coins };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (fCoins > 0) {
|
|
67
|
+
return { result: `${targetBtn.label} → +⏣ ${fCoins.toLocaleString()}`, coins: fCoins };
|
|
68
|
+
}
|
|
69
|
+
return { result: `${targetBtn.label}`, coins: 0 };
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
LOG.error(`[hl] Click error: ${e.message}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { result: 'done', coins: 0 };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {object} opts
|
|
80
|
+
* @param {object} opts.channel
|
|
81
|
+
* @param {function} opts.waitForDankMemer
|
|
82
|
+
* @returns {Promise<{result: string, coins: number}>}
|
|
83
|
+
*/
|
|
84
|
+
async function runHighLow({ channel, waitForDankMemer }) {
|
|
85
|
+
LOG.cmd(`${c.white}${c.bold}pls hl${c.reset}`);
|
|
86
|
+
|
|
87
|
+
await channel.send('pls hl');
|
|
88
|
+
const response = await waitForDankMemer(10000);
|
|
89
|
+
|
|
90
|
+
if (!response) {
|
|
91
|
+
LOG.warn('[hl] No response');
|
|
92
|
+
return { result: 'no response', coins: 0 };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isHoldTight(response)) {
|
|
96
|
+
const reason = getHoldTightReason(response);
|
|
97
|
+
LOG.warn(`[hl] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
|
|
98
|
+
await sleep(30000);
|
|
99
|
+
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
logMsg(response, 'hl');
|
|
103
|
+
const { result, coins } = await playHighLow(response);
|
|
104
|
+
|
|
105
|
+
if (coins > 0) {
|
|
106
|
+
LOG.coin(`[hl] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { result: `hl → ${result}`, coins };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { runHighLow };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hunt command handler.
|
|
3
|
+
* Send "pls hunt", detect if rifle is missing, auto-buy if needed.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
LOG, c, getFullText, parseCoins, logMsg, isHoldTight, getHoldTightReason, sleep, needsItem,
|
|
8
|
+
} = require('./utils');
|
|
9
|
+
const { buyItem } = require('./shop');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} opts
|
|
13
|
+
* @param {object} opts.channel
|
|
14
|
+
* @param {function} opts.waitForDankMemer
|
|
15
|
+
* @param {object} [opts.client] - Discord client for modal handling
|
|
16
|
+
* @returns {Promise<{result: string, coins: number, needsRifle: boolean}>}
|
|
17
|
+
*/
|
|
18
|
+
async function runHunt({ channel, waitForDankMemer, client }) {
|
|
19
|
+
LOG.cmd(`${c.white}${c.bold}pls hunt${c.reset}`);
|
|
20
|
+
|
|
21
|
+
await channel.send('pls hunt');
|
|
22
|
+
const response = await waitForDankMemer(10000);
|
|
23
|
+
|
|
24
|
+
if (!response) {
|
|
25
|
+
LOG.warn('[hunt] No response');
|
|
26
|
+
return { result: 'no response', coins: 0, needsRifle: false };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (isHoldTight(response)) {
|
|
30
|
+
const reason = getHoldTightReason(response);
|
|
31
|
+
LOG.warn(`[hunt] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
|
|
32
|
+
await sleep(30000);
|
|
33
|
+
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, needsRifle: false, holdTightReason: reason };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
logMsg(response, 'hunt');
|
|
37
|
+
const text = getFullText(response);
|
|
38
|
+
const textLower = text.toLowerCase();
|
|
39
|
+
|
|
40
|
+
// Check if we need a rifle
|
|
41
|
+
if (textLower.includes("don't have") || textLower.includes('need a') || textLower.includes('hunting rifle')) {
|
|
42
|
+
if (textLower.includes('rifle') || textLower.includes('hunt')) {
|
|
43
|
+
LOG.warn('[hunt] No rifle! Attempting to buy...');
|
|
44
|
+
|
|
45
|
+
const bought = await buyItem({
|
|
46
|
+
channel, waitForDankMemer, client,
|
|
47
|
+
itemName: 'Hunting Rifle',
|
|
48
|
+
quantity: 3,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (bought) {
|
|
52
|
+
LOG.success('[hunt] Rifle purchased! Re-running hunt...');
|
|
53
|
+
// Drain stale shop messages
|
|
54
|
+
while (await waitForDankMemer(1500)) { /* drain */ }
|
|
55
|
+
await sleep(1000);
|
|
56
|
+
|
|
57
|
+
await channel.send('pls hunt');
|
|
58
|
+
const r2 = await waitForDankMemer(10000);
|
|
59
|
+
if (r2) {
|
|
60
|
+
logMsg(r2, 'hunt-retry');
|
|
61
|
+
const t2 = getFullText(r2);
|
|
62
|
+
const coins = parseCoins(t2);
|
|
63
|
+
if (coins > 0) {
|
|
64
|
+
LOG.coin(`[hunt] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
65
|
+
return { result: `hunt → +⏣ ${coins.toLocaleString()}`, coins, needsRifle: false };
|
|
66
|
+
}
|
|
67
|
+
return { result: t2.substring(0, 60), coins: 0, needsRifle: false };
|
|
68
|
+
}
|
|
69
|
+
return { result: 'no response after rifle buy', coins: 0, needsRifle: false };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { result: 'need rifle (buy failed)', coins: 0, needsRifle: true };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const coins = parseCoins(text);
|
|
77
|
+
if (coins > 0) {
|
|
78
|
+
LOG.coin(`[hunt] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
79
|
+
return { result: `hunt → +⏣ ${coins.toLocaleString()}`, coins, needsRifle: false };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { result: text.substring(0, 60) || 'done', coins: 0, needsRifle: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { runHunt };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central export for all command handlers.
|
|
3
|
+
* Each command lives in its own file — import from here.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { runAdventure } = require('./adventure');
|
|
7
|
+
const { runBeg } = require('./beg');
|
|
8
|
+
const { runSearch, SAFE_SEARCH_LOCATIONS } = require('./search');
|
|
9
|
+
const { runCrime, SAFE_CRIME_OPTIONS } = require('./crime');
|
|
10
|
+
const { runHighLow } = require('./highlow');
|
|
11
|
+
const { runHunt } = require('./hunt');
|
|
12
|
+
const { runDig } = require('./dig');
|
|
13
|
+
const { runFish } = require('./fish');
|
|
14
|
+
const { runPostMemes } = require('./postmemes');
|
|
15
|
+
const { runScratch } = require('./scratch');
|
|
16
|
+
const { runBlackjack } = require('./blackjack');
|
|
17
|
+
const { runTrivia, triviaDB } = require('./trivia');
|
|
18
|
+
const { runWorkShift } = require('./work');
|
|
19
|
+
const { runCoinflip, runRoulette, runSlots, runSnakeeyes, runGamble } = require('./gamble');
|
|
20
|
+
const { runDeposit } = require('./deposit');
|
|
21
|
+
const { runGeneric, runAlert } = require('./generic');
|
|
22
|
+
const { buyItem, ITEM_COSTS } = require('./shop');
|
|
23
|
+
const { getPlayerLevel, meetsLevelRequirement } = require('./profile');
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
// Individual commands
|
|
27
|
+
runAdventure,
|
|
28
|
+
runBeg,
|
|
29
|
+
runSearch,
|
|
30
|
+
runCrime,
|
|
31
|
+
runHighLow,
|
|
32
|
+
runHunt,
|
|
33
|
+
runDig,
|
|
34
|
+
runFish,
|
|
35
|
+
runPostMemes,
|
|
36
|
+
runScratch,
|
|
37
|
+
runBlackjack,
|
|
38
|
+
runTrivia,
|
|
39
|
+
runWorkShift,
|
|
40
|
+
runCoinflip,
|
|
41
|
+
runRoulette,
|
|
42
|
+
runSlots,
|
|
43
|
+
runSnakeeyes,
|
|
44
|
+
runGamble,
|
|
45
|
+
runDeposit,
|
|
46
|
+
runGeneric,
|
|
47
|
+
runAlert,
|
|
48
|
+
buyItem,
|
|
49
|
+
|
|
50
|
+
// Profile / Level
|
|
51
|
+
getPlayerLevel,
|
|
52
|
+
meetsLevelRequirement,
|
|
53
|
+
|
|
54
|
+
// Constants
|
|
55
|
+
SAFE_SEARCH_LOCATIONS,
|
|
56
|
+
SAFE_CRIME_OPTIONS,
|
|
57
|
+
ITEM_COSTS,
|
|
58
|
+
triviaDB,
|
|
59
|
+
};
|