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.
@@ -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
+ };