dankgrinder 6.8.1 → 6.14.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/blackjack.js +110 -51
- package/lib/commands/crime.js +8 -0
- package/lib/commands/fish.js +7 -0
- package/lib/commands/gamble.js +70 -54
- package/lib/commands/highlow.js +48 -28
- package/lib/commands/hunt.js +153 -32
- package/lib/commands/profile.js +5 -4
- package/lib/commands/search.js +8 -0
- package/lib/commands/utils.js +24 -8
- package/lib/commands/work.js +92 -4
- package/lib/grinder.js +173 -30
- package/lib/rawLogger.js +541 -0
- package/package.json +1 -1
package/lib/commands/highlow.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HighLow command handler.
|
|
3
3
|
* Strategy: if hint > 50 → lower, if hint < 50 → higher, if exactly 50 → jackpot.
|
|
4
|
+
* Fast clicks with retry.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
const {
|
|
7
8
|
LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
|
|
8
|
-
logMsg, isHoldTight, getHoldTightReason, sleep,
|
|
9
|
+
logMsg, isHoldTight, getHoldTightReason, sleep,
|
|
9
10
|
} = require('./utils');
|
|
10
11
|
|
|
11
|
-
// Pre-compiled regex — avoid V8 recompilation on every call
|
|
12
12
|
const RE_HINT_BOLD = /hint.*?\*\*(\d+)\*\*/i;
|
|
13
13
|
const RE_NUMBER_BOLD = /number.*?\*\*(\d+)\*\*/i;
|
|
14
14
|
const RE_HINT_PLAIN = /hint.*?(\d+)/i;
|
|
15
15
|
const RE_NUMBER_PLAIN = /number.*?(\d+)/i;
|
|
16
16
|
const RE_STANDALONE_NUM = /\b(\d{1,3})\b/g;
|
|
17
17
|
const RE_HL_NET = /Net:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([+-]?[\d,]+)/i;
|
|
18
|
-
const RE_HL_WIN = /Winnings
|
|
18
|
+
const RE_HL_WIN = /(?:Winnings|won)\s*\*{0,2}\s*[⏣]?\s*\*{0,2}([\d,]+)/i;
|
|
19
|
+
const RE_HL_WON = /won\s+[⏣]\s*([\d,]+)/i;
|
|
19
20
|
|
|
20
21
|
function parseHintNumber(text) {
|
|
21
22
|
const hintMatch = text.match(RE_HINT_BOLD)
|
|
@@ -23,11 +24,13 @@ function parseHintNumber(text) {
|
|
|
23
24
|
|| text.match(RE_HINT_PLAIN)
|
|
24
25
|
|| text.match(RE_NUMBER_PLAIN);
|
|
25
26
|
if (hintMatch) return parseInt(hintMatch[1]);
|
|
27
|
+
|
|
28
|
+
// Fallback: find a number 1-100 in the text
|
|
26
29
|
RE_STANDALONE_NUM.lastIndex = 0;
|
|
27
30
|
const nums = text.match(RE_STANDALONE_NUM);
|
|
28
31
|
if (nums) {
|
|
29
|
-
for (
|
|
30
|
-
const v = parseInt(
|
|
32
|
+
for (const n of nums) {
|
|
33
|
+
const v = parseInt(n);
|
|
31
34
|
if (v >= 1 && v <= 100) return v;
|
|
32
35
|
}
|
|
33
36
|
}
|
|
@@ -35,6 +38,9 @@ function parseHintNumber(text) {
|
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
function parseNetCoins(text) {
|
|
41
|
+
// Try "You won ⏣ 6,303" first
|
|
42
|
+
const wonMatch = text.match(RE_HL_WON);
|
|
43
|
+
if (wonMatch) return parseInt(wonMatch[1].replace(/,/g, ''));
|
|
38
44
|
const netMatch = text.match(RE_HL_NET);
|
|
39
45
|
if (netMatch) return parseInt(netMatch[1].replace(/,/g, ''));
|
|
40
46
|
const winMatch = text.match(RE_HL_WIN);
|
|
@@ -42,12 +48,31 @@ function parseNetCoins(text) {
|
|
|
42
48
|
return 0;
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
// Fast click with retry
|
|
52
|
+
async function clickFast(msg, btn, retries = 2) {
|
|
53
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
54
|
+
try {
|
|
55
|
+
await sleep(50 + Math.random() * 100); // 50-150ms
|
|
56
|
+
const result = await safeClickButton(msg, btn);
|
|
57
|
+
if (result) return result;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (attempt === retries) {
|
|
60
|
+
LOG.error(`[hl] Click failed: ${e.message}`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
await sleep(200);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
45
69
|
async function playHighLow(response, depth = 0) {
|
|
46
70
|
if (!response || depth > 5) return { result: 'done', coins: 0 };
|
|
47
71
|
|
|
48
72
|
const text = getFullText(response);
|
|
49
73
|
const buttons = getAllButtons(response);
|
|
50
74
|
|
|
75
|
+
// Game over — no buttons or all disabled
|
|
51
76
|
if (buttons.length === 0 || buttons.every(b => b.disabled)) {
|
|
52
77
|
const net = parseNetCoins(text);
|
|
53
78
|
const coins = net > 0 ? net : parseCoins(text);
|
|
@@ -64,7 +89,7 @@ async function playHighLow(response, depth = 0) {
|
|
|
64
89
|
} else if (hint < 50) {
|
|
65
90
|
targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('higher'));
|
|
66
91
|
} else {
|
|
67
|
-
//
|
|
92
|
+
// 50 = jackpot attempt, fallback to higher
|
|
68
93
|
targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('jackpot'))
|
|
69
94
|
|| buttons.find(b => (b.label || '').toLowerCase().includes('higher'));
|
|
70
95
|
}
|
|
@@ -72,7 +97,7 @@ async function playHighLow(response, depth = 0) {
|
|
|
72
97
|
LOG.info(`[hl] Hint: ${hint} → ${targetBtn?.label || '?'}`);
|
|
73
98
|
} else {
|
|
74
99
|
targetBtn = buttons.find(b => !b.disabled) || buttons[0];
|
|
75
|
-
LOG.info(`[hl] No hint
|
|
100
|
+
LOG.info(`[hl] No hint → ${targetBtn?.label || '?'}`);
|
|
76
101
|
}
|
|
77
102
|
|
|
78
103
|
if (!targetBtn || targetBtn.disabled) {
|
|
@@ -80,27 +105,22 @@ async function playHighLow(response, depth = 0) {
|
|
|
80
105
|
return { result: 'buttons disabled', coins };
|
|
81
106
|
}
|
|
82
107
|
|
|
83
|
-
await
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
if (followUp) {
|
|
88
|
-
logMsg(followUp, `hl-round-${depth}`);
|
|
89
|
-
const fText = getFullText(followUp);
|
|
90
|
-
|
|
91
|
-
const moreButtons = getAllButtons(followUp);
|
|
92
|
-
if (moreButtons.length >= 2 && !moreButtons.every(b => b.disabled)) {
|
|
93
|
-
return playHighLow(followUp, depth + 1);
|
|
94
|
-
}
|
|
108
|
+
const followUp = await clickFast(response, targetBtn);
|
|
109
|
+
if (followUp) {
|
|
110
|
+
logMsg(followUp, `hl-round-${depth}`);
|
|
111
|
+
const fText = getFullText(followUp);
|
|
95
112
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return { result: `${targetBtn.label} → ${coins > 0 ? '+⏣ ' + coins.toLocaleString() : 'done'}`, coins, lost };
|
|
113
|
+
// Check if there are more rounds
|
|
114
|
+
const moreButtons = getAllButtons(followUp);
|
|
115
|
+
if (moreButtons.length >= 2 && !moreButtons.every(b => b.disabled)) {
|
|
116
|
+
return playHighLow(followUp, depth + 1);
|
|
101
117
|
}
|
|
102
|
-
|
|
103
|
-
|
|
118
|
+
|
|
119
|
+
// Final round — parse result
|
|
120
|
+
const net = parseNetCoins(fText);
|
|
121
|
+
const coins = net > 0 ? net : parseCoins(fText);
|
|
122
|
+
const lost = net < 0 ? Math.abs(net) : 0;
|
|
123
|
+
return { result: `${targetBtn.label} → ${coins > 0 ? '+⏣ ' + coins.toLocaleString() : 'done'}`, coins, lost };
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
return { result: 'done', coins: 0 };
|
|
@@ -110,7 +130,7 @@ async function runHighLow({ channel, waitForDankMemer }) {
|
|
|
110
130
|
LOG.cmd(`${c.white}${c.bold}pls hl${c.reset}`);
|
|
111
131
|
|
|
112
132
|
await channel.send('pls hl');
|
|
113
|
-
const response = await waitForDankMemer(
|
|
133
|
+
const response = await waitForDankMemer(12000);
|
|
114
134
|
|
|
115
135
|
if (!response) {
|
|
116
136
|
LOG.warn('[hl] No response');
|
|
@@ -119,7 +139,7 @@ async function runHighLow({ channel, waitForDankMemer }) {
|
|
|
119
139
|
|
|
120
140
|
if (isHoldTight(response)) {
|
|
121
141
|
const reason = getHoldTightReason(response);
|
|
122
|
-
LOG.warn(`[hl] Hold Tight${reason ? ` (
|
|
142
|
+
LOG.warn(`[hl] Hold Tight${reason ? ` (/${reason})` : ''} — waiting 30s`);
|
|
123
143
|
await sleep(30000);
|
|
124
144
|
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
|
|
125
145
|
}
|
package/lib/commands/hunt.js
CHANGED
|
@@ -1,26 +1,87 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hunt command handler.
|
|
3
3
|
* Send "pls hunt", detect if rifle is missing, auto-buy if needed.
|
|
4
|
+
* Handles dragon fireball dodge minigame (Left/Middle/Right).
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
const {
|
|
7
|
-
LOG, c, getFullText, parseCoins,
|
|
8
|
+
LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
|
|
9
|
+
logMsg, isHoldTight, getHoldTightReason, sleep, needsItem,
|
|
8
10
|
isCV2, ensureCV2, stripAnsi,
|
|
9
11
|
} = require('./utils');
|
|
10
12
|
const { buyItem } = require('./shop');
|
|
11
13
|
|
|
14
|
+
// Fast click with retry
|
|
15
|
+
async function clickFast(msg, btn, retries = 2) {
|
|
16
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
17
|
+
try {
|
|
18
|
+
await sleep(50 + Math.random() * 100);
|
|
19
|
+
const result = await safeClickButton(msg, btn);
|
|
20
|
+
if (result) return result;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
if (attempt === retries) {
|
|
23
|
+
LOG.error(`[hunt] Click failed: ${e.message}`);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
await sleep(200);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
12
32
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
33
|
+
* Dragon fireball dodge minigame.
|
|
34
|
+
* The embed description has emoji layout showing where the dragon is.
|
|
35
|
+
* Dragon can be on left, middle, or right — pick a different lane.
|
|
36
|
+
*
|
|
37
|
+
* Pattern: Dragon emoji in the description indicates its position.
|
|
38
|
+
* - If dragon is on the left side → pick Middle or Right
|
|
39
|
+
* - If dragon is on the right side → pick Left or Middle
|
|
40
|
+
* - If dragon is in the middle → pick Left or Right
|
|
41
|
+
* - Default: pick randomly
|
|
18
42
|
*/
|
|
43
|
+
function detectDragonPosition(text) {
|
|
44
|
+
const lower = (text || '').toLowerCase();
|
|
45
|
+
// Look at the line with the dragon emoji
|
|
46
|
+
// Format: # <emptyspace><emptyspace><Dragon> (right)
|
|
47
|
+
// Format: # <Dragon><emptyspace><emptyspace> (left)
|
|
48
|
+
// Format: # <emptyspace><Dragon><emptyspace> (middle)
|
|
49
|
+
const lines = text.split('\n');
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
if (!line.includes('Dragon') && !line.includes('dragon') && !line.includes('Fireball') && !line.includes('fireball')) continue;
|
|
52
|
+
// Count position of dragon relative to emptyspace
|
|
53
|
+
const parts = line.split(/[<>]+/).filter(p => p.includes('Dragon') || p.includes('emptyspace'));
|
|
54
|
+
let dragonIdx = -1;
|
|
55
|
+
let total = 0;
|
|
56
|
+
for (let i = 0; i < parts.length; i++) {
|
|
57
|
+
if (parts[i].includes('Dragon') || parts[i].includes('dragon')) dragonIdx = total;
|
|
58
|
+
total++;
|
|
59
|
+
}
|
|
60
|
+
if (dragonIdx === 0) return 'left';
|
|
61
|
+
if (dragonIdx >= total - 1) return 'right';
|
|
62
|
+
return 'middle';
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function pickDodgeLane(dragonPos, buttons) {
|
|
68
|
+
const labels = buttons.map(b => (b.label || '').toLowerCase());
|
|
69
|
+
const leftBtn = buttons.find(b => (b.label || '').toLowerCase() === 'left');
|
|
70
|
+
const midBtn = buttons.find(b => (b.label || '').toLowerCase() === 'middle');
|
|
71
|
+
const rightBtn = buttons.find(b => (b.label || '').toLowerCase() === 'right');
|
|
72
|
+
|
|
73
|
+
if (dragonPos === 'left') return midBtn || rightBtn || buttons[1] || buttons[0];
|
|
74
|
+
if (dragonPos === 'right') return leftBtn || midBtn || buttons[0] || buttons[1];
|
|
75
|
+
if (dragonPos === 'middle') return leftBtn || rightBtn || buttons[0] || buttons[2];
|
|
76
|
+
// Unknown position — pick randomly from non-middle (safer)
|
|
77
|
+
return buttons[Math.floor(Math.random() * buttons.length)];
|
|
78
|
+
}
|
|
79
|
+
|
|
19
80
|
async function runHunt({ channel, waitForDankMemer, client }) {
|
|
20
81
|
LOG.cmd(`${c.white}${c.bold}pls hunt${c.reset}`);
|
|
21
82
|
|
|
22
83
|
await channel.send('pls hunt');
|
|
23
|
-
const response = await waitForDankMemer(
|
|
84
|
+
const response = await waitForDankMemer(12000);
|
|
24
85
|
|
|
25
86
|
if (!response) {
|
|
26
87
|
LOG.warn('[hunt] No response');
|
|
@@ -29,7 +90,7 @@ async function runHunt({ channel, waitForDankMemer, client }) {
|
|
|
29
90
|
|
|
30
91
|
if (isHoldTight(response)) {
|
|
31
92
|
const reason = getHoldTightReason(response);
|
|
32
|
-
LOG.warn(`[hunt] Hold Tight${reason ? ` (
|
|
93
|
+
LOG.warn(`[hunt] Hold Tight${reason ? ` (/${reason})` : ''} — waiting 30s`);
|
|
33
94
|
await sleep(30000);
|
|
34
95
|
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, needsRifle: false, holdTightReason: reason };
|
|
35
96
|
}
|
|
@@ -39,47 +100,107 @@ async function runHunt({ channel, waitForDankMemer, client }) {
|
|
|
39
100
|
const text = getFullText(response);
|
|
40
101
|
const cleanText = stripAnsi(text).replace(/\s+/g, ' ').trim();
|
|
41
102
|
const textLower = cleanText.toLowerCase();
|
|
42
|
-
const missing = needsItem(cleanText);
|
|
43
103
|
|
|
104
|
+
// Check for missing rifle
|
|
105
|
+
const missing = needsItem(cleanText);
|
|
44
106
|
const rifleMissing = missing === 'hunting rifle'
|
|
45
|
-
|| /(?:don['
|
|
107
|
+
|| /(?:don['']?t have|do not have|need|needs|requires?|missing|must have|you need|lack)\s+(?:a\s+)?(?:hunting\s+)?rifle/.test(textLower)
|
|
46
108
|
|| ((textLower.includes('following items') || textLower.includes('missing items')) && textLower.includes('rifle'));
|
|
47
109
|
|
|
48
|
-
// Check if we need a rifle
|
|
49
110
|
if (rifleMissing) {
|
|
50
|
-
LOG.warn('[hunt] No rifle!
|
|
51
|
-
|
|
52
|
-
const bought = await buyItem({
|
|
53
|
-
channel, waitForDankMemer, client,
|
|
54
|
-
itemName: 'Hunting Rifle',
|
|
55
|
-
quantity: 1,
|
|
56
|
-
});
|
|
57
|
-
|
|
111
|
+
LOG.warn('[hunt] No rifle! Buying...');
|
|
112
|
+
const bought = await buyItem({ channel, waitForDankMemer, client, itemName: 'Hunting Rifle', quantity: 1 });
|
|
58
113
|
if (bought) {
|
|
59
|
-
LOG.success('[hunt] Rifle
|
|
60
|
-
|
|
61
|
-
while (await waitForDankMemer(1500)) { /* drain */ }
|
|
114
|
+
LOG.success('[hunt] Rifle bought! Retrying...');
|
|
115
|
+
while (await waitForDankMemer(1500)) {}
|
|
62
116
|
await sleep(1000);
|
|
63
|
-
|
|
64
117
|
await channel.send('pls hunt');
|
|
65
|
-
const r2 = await waitForDankMemer(
|
|
118
|
+
const r2 = await waitForDankMemer(12000);
|
|
66
119
|
if (r2) {
|
|
67
120
|
if (isCV2(r2)) await ensureCV2(r2);
|
|
68
121
|
logMsg(r2, 'hunt-retry');
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (c2 > 0) {
|
|
72
|
-
LOG.coin(`[hunt] ${c.green}+⏣ ${c2.toLocaleString()}${c.reset}`);
|
|
73
|
-
return { result: `hunt → +⏣ ${c2.toLocaleString()}`, coins: c2, needsRifle: false };
|
|
74
|
-
}
|
|
75
|
-
return { result: stripAnsi(t2).replace(/\s+/g, ' ').trim().substring(0, 60), coins: 0, needsRifle: false };
|
|
122
|
+
// Handle minigame on retry too
|
|
123
|
+
return await handleHuntResponse(r2, channel, waitForDankMemer);
|
|
76
124
|
}
|
|
77
125
|
return { result: 'no response after rifle buy', coins: 0, needsRifle: false };
|
|
78
126
|
}
|
|
79
|
-
|
|
80
127
|
return { result: 'need rifle (buy failed)', coins: 0, needsRifle: true };
|
|
81
128
|
}
|
|
82
129
|
|
|
130
|
+
return await handleHuntResponse(response, channel, waitForDankMemer);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function handleHuntResponse(response, channel, waitForDankMemer) {
|
|
134
|
+
const text = getFullText(response);
|
|
135
|
+
const cleanText = stripAnsi(text).replace(/\s+/g, ' ').trim();
|
|
136
|
+
const textLower = cleanText.toLowerCase();
|
|
137
|
+
|
|
138
|
+
// Check for minigame buttons (dragon dodge, etc.)
|
|
139
|
+
const buttons = getAllButtons(response).filter(b => !b.disabled && b.label);
|
|
140
|
+
if (buttons.length >= 2) {
|
|
141
|
+
const labels = buttons.map(b => (b.label || '').toLowerCase());
|
|
142
|
+
const isDragonDodge = textLower.includes('dragon') || textLower.includes('fireball') || textLower.includes('dodge');
|
|
143
|
+
const isLaneGame = labels.includes('left') || labels.includes('middle') || labels.includes('right');
|
|
144
|
+
|
|
145
|
+
if (isDragonDodge && isLaneGame) {
|
|
146
|
+
// Dragon fireball dodge
|
|
147
|
+
const dragonPos = detectDragonPosition(text);
|
|
148
|
+
const btn = pickDodgeLane(dragonPos, buttons);
|
|
149
|
+
LOG.info(`[hunt] Dragon at ${dragonPos || '?'} → dodging "${btn.label}"`);
|
|
150
|
+
const followUp = await clickFast(response, btn);
|
|
151
|
+
if (followUp) {
|
|
152
|
+
logMsg(followUp, 'hunt-dodge');
|
|
153
|
+
const fText = getFullText(followUp);
|
|
154
|
+
const coins = parseCoins(fText);
|
|
155
|
+
if (coins > 0) {
|
|
156
|
+
LOG.coin(`[hunt] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
157
|
+
return { result: `hunt dodge → +⏣ ${coins.toLocaleString()}`, coins, needsRifle: false };
|
|
158
|
+
}
|
|
159
|
+
return { result: stripAnsi(fText).replace(/\s+/g, ' ').trim().substring(0, 60) || 'dodged', coins: 0, needsRifle: false };
|
|
160
|
+
}
|
|
161
|
+
// Click didn't return followup — wait for edit
|
|
162
|
+
await sleep(2000);
|
|
163
|
+
try {
|
|
164
|
+
const edited = await channel.messages.fetch(response.id);
|
|
165
|
+
if (edited) {
|
|
166
|
+
const eText = getFullText(edited);
|
|
167
|
+
const coins = parseCoins(eText);
|
|
168
|
+
if (coins > 0) {
|
|
169
|
+
LOG.coin(`[hunt] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
170
|
+
return { result: `hunt dodge → +⏣ ${coins.toLocaleString()}`, coins, needsRifle: false };
|
|
171
|
+
}
|
|
172
|
+
return { result: stripAnsi(eText).replace(/\s+/g, ' ').trim().substring(0, 60) || 'dodged', coins: 0, needsRifle: false };
|
|
173
|
+
}
|
|
174
|
+
} catch {}
|
|
175
|
+
} else {
|
|
176
|
+
// Generic minigame — click first available button
|
|
177
|
+
const btn = buttons[0];
|
|
178
|
+
LOG.info(`[hunt] Minigame → clicking "${btn.label}"`);
|
|
179
|
+
const followUp = await clickFast(response, btn);
|
|
180
|
+
if (followUp) {
|
|
181
|
+
logMsg(followUp, 'hunt-minigame');
|
|
182
|
+
const fText = getFullText(followUp);
|
|
183
|
+
const coins = parseCoins(fText);
|
|
184
|
+
return { result: stripAnsi(fText).replace(/\s+/g, ' ').trim().substring(0, 60), coins: coins || 0, needsRifle: false };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Wait for message update (hunt animates sometimes)
|
|
190
|
+
await sleep(2000);
|
|
191
|
+
try {
|
|
192
|
+
const edited = await channel.messages.fetch(response.id);
|
|
193
|
+
if (edited) {
|
|
194
|
+
const eText = getFullText(edited);
|
|
195
|
+
const coins = parseCoins(eText);
|
|
196
|
+
if (coins > 0) {
|
|
197
|
+
LOG.coin(`[hunt] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
198
|
+
return { result: `hunt → +⏣ ${coins.toLocaleString()}`, coins, needsRifle: false };
|
|
199
|
+
}
|
|
200
|
+
return { result: stripAnsi(eText).replace(/\s+/g, ' ').trim().substring(0, 60) || 'done', coins: 0, needsRifle: false };
|
|
201
|
+
}
|
|
202
|
+
} catch {}
|
|
203
|
+
|
|
83
204
|
const coins = parseCoins(cleanText);
|
|
84
205
|
if (coins > 0) {
|
|
85
206
|
LOG.coin(`[hunt] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
package/lib/commands/profile.js
CHANGED
|
@@ -12,8 +12,8 @@ const {
|
|
|
12
12
|
const levelCache = {};
|
|
13
13
|
const CACHE_TTL = 10 * 60 * 1000; // 10 minutes
|
|
14
14
|
|
|
15
|
-
const RE_PROFILE_LEVEL = /(?:level|lvl)\s*:?\s
|
|
16
|
-
const RE_PROFILE_PRESTIGE_LEVEL = /prestige\s+\d+\s+level\s
|
|
15
|
+
const RE_PROFILE_LEVEL = /(?:level|lvl)\s*:?\s*`?(\d+)`?/i;
|
|
16
|
+
const RE_PROFILE_PRESTIGE_LEVEL = /prestige\s+\d+\s+level\s+`?(\d+)`?/i;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Parse level from profile response text.
|
|
@@ -77,9 +77,10 @@ async function getPlayerLevel({ channel, waitForDankMemer, accountId = 'default'
|
|
|
77
77
|
if (level !== null) {
|
|
78
78
|
LOG.info(`[profile] Level: ${c.bold}${level}${c.reset}`);
|
|
79
79
|
levelCache[accountId] = { level, checkedAt: Date.now() };
|
|
80
|
-
// Persist to Redis for
|
|
80
|
+
// Persist to Redis — longer TTL for higher levels since they only go up
|
|
81
81
|
if (redis) {
|
|
82
|
-
|
|
82
|
+
const ttl = level >= 25 ? 2592000 : 600; // 30 days if ≥25, 10 min otherwise
|
|
83
|
+
try { await redis.set(`dkg:level:${accountId}`, String(level), 'EX', ttl); } catch {}
|
|
83
84
|
}
|
|
84
85
|
} else {
|
|
85
86
|
LOG.warn(`[profile] Could not parse level from: ${text.substring(0, 100)}`);
|
package/lib/commands/search.js
CHANGED
|
@@ -130,6 +130,14 @@ async function runSearch({ channel, waitForDankMemer, safeAnswers }) {
|
|
|
130
130
|
const locKey = (btn.label || '').toLowerCase();
|
|
131
131
|
const prev = locationEarnings.get(locKey) || 0;
|
|
132
132
|
locationEarnings.set(locKey, prev + coins);
|
|
133
|
+
|
|
134
|
+
// Check for death
|
|
135
|
+
const textLower = text.toLowerCase();
|
|
136
|
+
if (textLower.includes('you died') || textLower.includes('lifesaver protected')) {
|
|
137
|
+
LOG.error(`[search] DEATH DETECTED! Lifesaver may have been used.`);
|
|
138
|
+
return { result: `you died during search`, coins: 0, died: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
133
141
|
if (coins > 0) {
|
|
134
142
|
LOG.coin(`[search] ${btn.label} → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
135
143
|
return { result: `${btn.label} → +⏣ ${coins.toLocaleString()}`, coins };
|
package/lib/commands/utils.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
const https = require('https');
|
|
13
13
|
const { AhoCorasick, LRUCache, StringPool, ObjectPool } = require('../structures');
|
|
14
|
+
const rawLogger = require('../rawLogger');
|
|
14
15
|
|
|
15
16
|
// ── HTTPS Keep-Alive Agent ───────────────────────────────────
|
|
16
17
|
// At 10K accounts, every CV2 fetch without keep-alive creates a new
|
|
@@ -470,17 +471,29 @@ async function ensureCV2(msg, force = false) {
|
|
|
470
471
|
delete msg._cv2EditedTs;
|
|
471
472
|
cv2Cache.delete(msg.id);
|
|
472
473
|
}
|
|
474
|
+
|
|
475
|
+
// ── PRIMARY: Use raw gateway logger (always has CV2 data) ──
|
|
476
|
+
const rawParsed = rawLogger.getRawMessage(msg.id);
|
|
477
|
+
if (rawParsed && rawParsed.components?.length > 0) {
|
|
478
|
+
msg._cv2 = rawParsed.components;
|
|
479
|
+
msg._cv2text = rawParsed.cv2Text || _extractCV2Text(rawParsed.components).trim();
|
|
480
|
+
msg._cv2buttons = rawParsed.buttons?.length > 0
|
|
481
|
+
? rawParsed.buttons.map(b => ({ type: 'BUTTON', label: b.label, customId: b.customId, style: b.style, url: null, disabled: b.disabled, emoji: b.emoji, _raw: b }))
|
|
482
|
+
: _extractCV2Buttons(rawParsed.components);
|
|
483
|
+
msg._cv2EditedTs = msgEditedTs;
|
|
484
|
+
cv2Cache.set(msg.id, { components: rawParsed.components, editedTimestamp: msgEditedTs });
|
|
485
|
+
return msg;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ── FALLBACK: LRU cache ──
|
|
473
489
|
const token = msg.client?.token;
|
|
474
490
|
const chId = msg.channelId || msg.channel?.id;
|
|
475
491
|
if (!token || !chId) return msg;
|
|
476
492
|
|
|
477
|
-
// LRU cache hit — O(1) lookup avoids redundant HTTP fetches
|
|
478
493
|
const cached = cv2Cache.get(msg.id);
|
|
479
494
|
if (cached && !force) {
|
|
480
495
|
const cachedComponents = Array.isArray(cached) ? cached : cached.components;
|
|
481
496
|
const cachedEditedTs = Array.isArray(cached) ? null : (cached.editedTimestamp || null);
|
|
482
|
-
|
|
483
|
-
// If message has been edited since cache snapshot, ignore stale cache.
|
|
484
497
|
if (!msgEditedTs || cachedEditedTs === msgEditedTs) {
|
|
485
498
|
msg._cv2 = cachedComponents;
|
|
486
499
|
msg._cv2text = _extractCV2Text(cachedComponents).trim();
|
|
@@ -490,6 +503,7 @@ async function ensureCV2(msg, force = false) {
|
|
|
490
503
|
}
|
|
491
504
|
}
|
|
492
505
|
|
|
506
|
+
// ── LAST RESORT: HTTP fetch (may fail for selfbot tokens) ──
|
|
493
507
|
let raw = null;
|
|
494
508
|
try {
|
|
495
509
|
raw = await _httpGet(
|
|
@@ -501,11 +515,13 @@ async function ensureCV2(msg, force = false) {
|
|
|
501
515
|
}
|
|
502
516
|
|
|
503
517
|
if (!raw || !raw.components) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
518
|
+
try {
|
|
519
|
+
const msgs = await _httpGet(
|
|
520
|
+
`https://discord.com/api/v9/channels/${chId}/messages?limit=5&around=${msg.id}`,
|
|
521
|
+
{ Authorization: token }
|
|
522
|
+
);
|
|
523
|
+
raw = Array.isArray(msgs) ? msgs.find(m => m.id === msg.id) : null;
|
|
524
|
+
} catch { raw = null; }
|
|
509
525
|
}
|
|
510
526
|
|
|
511
527
|
if (raw?.components) {
|
package/lib/commands/work.js
CHANGED
|
@@ -27,6 +27,25 @@ const RE_WORK_COOLDOWN_TS = /<t:(\d+)(?::[tTdDfFR])?>/g;
|
|
|
27
27
|
const RE_WORK_COOLDOWN_MINUTES = /(\d+)\s*minute/i;
|
|
28
28
|
const RE_WORK_COOLDOWN_HOURS = /(\d+)\s*hour/i;
|
|
29
29
|
|
|
30
|
+
// Color-word pair parser for color memory minigame
|
|
31
|
+
// Format: <:Black:123> `toddler` <:Green:456> `pacifier` <:White:789> `behave`
|
|
32
|
+
const RE_COLOR_WORD = /<:(\w+):\d+>\s*`([^`]+)`/g;
|
|
33
|
+
const KNOWN_COLORS = new Set(['black', 'green', 'white', 'yellow', 'red', 'blue', 'purple', 'orange', 'pink']);
|
|
34
|
+
|
|
35
|
+
function parseColorWordPairs(text) {
|
|
36
|
+
const pairs = {};
|
|
37
|
+
let m;
|
|
38
|
+
const re = new RegExp(RE_COLOR_WORD.source, 'g');
|
|
39
|
+
while ((m = re.exec(text)) !== null) {
|
|
40
|
+
const color = m[1].toLowerCase();
|
|
41
|
+
const word = m[2].toLowerCase().trim();
|
|
42
|
+
if (KNOWN_COLORS.has(color)) {
|
|
43
|
+
pairs[word] = color;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return pairs;
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
function normalizeLower(text) {
|
|
31
50
|
return String(text || '')
|
|
32
51
|
.normalize('NFKC')
|
|
@@ -285,6 +304,63 @@ async function handleRepeatWord({ current, wordOrder }) {
|
|
|
285
304
|
return current;
|
|
286
305
|
}
|
|
287
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Handle the "Color Memory" minigame.
|
|
309
|
+
* Phase 1: Shows color-word pairs like <:Black:123> `toddler` <:Green:456> `pacifier`
|
|
310
|
+
* Phase 2: "What color was next to the word `behave`?" with color buttons
|
|
311
|
+
*
|
|
312
|
+
* We parse the pairs from Phase 1's raw embed description, then answer Phase 2.
|
|
313
|
+
*/
|
|
314
|
+
async function handleColorMemory({ channel, current, colorWordPairs, waitForDankMemer }) {
|
|
315
|
+
LOG.info(`[work] Color pairs: ${JSON.stringify(colorWordPairs)}`);
|
|
316
|
+
const msgId = current.id;
|
|
317
|
+
|
|
318
|
+
// Wait for Phase 2 — "What color was next to the word X?"
|
|
319
|
+
let phase2 = await waitForDankMemer(8000);
|
|
320
|
+
if (!phase2) phase2 = await refetchMsg(channel, msgId);
|
|
321
|
+
if (!phase2) {
|
|
322
|
+
LOG.warn('[work] No Phase 2 for color memory');
|
|
323
|
+
return current;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
logMsg(phase2, 'work-color-phase2');
|
|
327
|
+
current = phase2;
|
|
328
|
+
|
|
329
|
+
const p2Text = getFullText(current);
|
|
330
|
+
const p2Lower = p2Text.toLowerCase();
|
|
331
|
+
|
|
332
|
+
// Extract the word being asked about: "What color was next to the word `behave`?"
|
|
333
|
+
const wordMatch = p2Text.match(/word\s*`([^`]+)`/i);
|
|
334
|
+
if (!wordMatch) {
|
|
335
|
+
LOG.warn('[work] Could not parse asked word from color memory');
|
|
336
|
+
return await handleGenericMinigame({ channel, current, waitForDankMemer });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const askedWord = wordMatch[1].toLowerCase().trim();
|
|
340
|
+
const correctColor = colorWordPairs[askedWord];
|
|
341
|
+
LOG.info(`[work] Asked: "${askedWord}" → color: "${correctColor || '?'}"`);
|
|
342
|
+
|
|
343
|
+
if (correctColor) {
|
|
344
|
+
const buttons = getAllButtons(current);
|
|
345
|
+
const btn = buttons.find(b => !b.disabled && (b.label || '').toLowerCase() === correctColor);
|
|
346
|
+
if (btn) {
|
|
347
|
+
LOG.info(`[work] Clicking "${btn.label}"`);
|
|
348
|
+
await sleep(50 + Math.random() * 100);
|
|
349
|
+
try {
|
|
350
|
+
const result = await safeClickButton(current, btn);
|
|
351
|
+
if (result) return result;
|
|
352
|
+
} catch (e) { LOG.error(`[work] Color click error: ${e.message}`); }
|
|
353
|
+
// Fallback: wait for update
|
|
354
|
+
await sleep(1500);
|
|
355
|
+
const updated = await refetchMsg(channel, msgId);
|
|
356
|
+
if (updated) return updated;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Fallback: generic click
|
|
361
|
+
return await handleGenericMinigame({ channel, current, waitForDankMemer });
|
|
362
|
+
}
|
|
363
|
+
|
|
288
364
|
/**
|
|
289
365
|
* Handle generic button-clicking minigame (color match, emoji match, etc.)
|
|
290
366
|
* Falls back to clicking buttons in display order if we can't determine the game type.
|
|
@@ -385,7 +461,17 @@ async function runWorkShift({ channel, waitForDankMemer }) {
|
|
|
385
461
|
const tLower = text.toLowerCase();
|
|
386
462
|
const wordOrder = parseMemoryOrder(text);
|
|
387
463
|
|
|
388
|
-
|
|
464
|
+
// Get raw embed description (preserves <:Color:ID> emoji format for color memory)
|
|
465
|
+
const rawEmbedDesc = (current.embeds?.[0]?.description || current.embeds?.[0]?.data?.description || '');
|
|
466
|
+
const colorWordPairs = parseColorWordPairs(rawEmbedDesc);
|
|
467
|
+
const hasColorPairs = Object.keys(colorWordPairs).length >= 2;
|
|
468
|
+
|
|
469
|
+
if (hasColorPairs && (tLower.includes('color') || tLower.includes('look at each color') || tLower.includes('closely'))) {
|
|
470
|
+
// Color Memory minigame — Phase 1: memorize color-word pairs
|
|
471
|
+
LOG.info(`[work] Minigame: Color Memory (${Object.keys(colorWordPairs).length} pairs)`);
|
|
472
|
+
current = await handleColorMemory({ channel, current, colorWordPairs, waitForDankMemer });
|
|
473
|
+
|
|
474
|
+
} else if (wordOrder.length > 0 && tLower.includes('remember')) {
|
|
389
475
|
// Word Memory minigame — Phase 1: memorize order
|
|
390
476
|
LOG.info(`[work] Minigame: Word Memory (${wordOrder.length} words)`);
|
|
391
477
|
current = await handleWordMemory({ channel, current, wordOrder, waitForDankMemer });
|
|
@@ -408,13 +494,15 @@ async function runWorkShift({ channel, waitForDankMemer }) {
|
|
|
408
494
|
current = phase2;
|
|
409
495
|
logMsg(current, 'work-phase2');
|
|
410
496
|
const p2Text = getFullText(current).toLowerCase();
|
|
497
|
+
const p2RawDesc = (current.embeds?.[0]?.description || current.embeds?.[0]?.data?.description || '');
|
|
411
498
|
|
|
412
|
-
if
|
|
413
|
-
|
|
499
|
+
// Check if Phase 2 is a color memory question
|
|
500
|
+
if (p2Text.includes('what color') && hasColorPairs) {
|
|
501
|
+
current = await handleColorMemory({ channel, current: phase2, colorWordPairs, waitForDankMemer: async () => null });
|
|
502
|
+
} else if (p2Text.includes('correct order') || p2Text.includes('click the buttons')) {
|
|
414
503
|
if (wordOrder.length > 0) {
|
|
415
504
|
current = await handleWordMemory({ channel, current, wordOrder, waitForDankMemer });
|
|
416
505
|
} else {
|
|
417
|
-
// Re-parse from Phase 2 if word order was in Phase 1 content
|
|
418
506
|
current = await handleGenericMinigame({ channel, current, waitForDankMemer });
|
|
419
507
|
}
|
|
420
508
|
} else {
|