dankgrinder 5.0.0 → 5.0.1
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/adventure.js +25 -14
- package/lib/commands/beg.js +3 -1
- package/lib/commands/blackjack.js +30 -15
- package/lib/commands/crime.js +7 -3
- package/lib/commands/drops.js +29 -14
- package/lib/commands/fish.js +9 -4
- package/lib/commands/gamble.js +4 -1
- package/lib/commands/highlow.js +19 -11
- package/lib/commands/postmemes.js +3 -1
- package/lib/commands/profile.js +5 -4
- package/lib/commands/search.js +2 -2
- package/lib/commands/stream.js +1 -1
- package/lib/commands/trivia.js +6 -2
- package/lib/commands/utils.js +100 -63
- package/lib/commands/work.js +17 -8
- package/lib/grinder.js +152 -42
- package/lib/structures.js +131 -0
- package/package.json +1 -1
|
@@ -39,6 +39,9 @@ const {
|
|
|
39
39
|
safeClickButton, isHoldTight, logMsg,
|
|
40
40
|
} = require('./utils');
|
|
41
41
|
|
|
42
|
+
const RE_DISCORD_TIMESTAMP = /<t:(\d+)(?::[tTdDfFR])?>/;
|
|
43
|
+
const RE_ADVENTURE_AGAIN_LABEL = /adventure again in (\d+)\s*(minute|min|hour|second)/;
|
|
44
|
+
|
|
42
45
|
// ── Adventure type rotation (cycle through all types each run) ────
|
|
43
46
|
let lastAdventureIndex = -1;
|
|
44
47
|
|
|
@@ -102,8 +105,9 @@ function isAdventureDone(msg) {
|
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
// ── Safe choices: prefer non-destructive options ─────────────────
|
|
105
|
-
const SAFE_KEYWORDS = ['flee', 'run', 'hide', 'avoid', 'ignore', 'leave', 'walk away', 'back away', 'retreat', 'skip'];
|
|
106
|
-
const RISKY_KEYWORDS = ['reach', 'grab', 'fight', 'attack', 'steal', 'open', 'touch', 'eat', 'drink'];
|
|
108
|
+
const SAFE_KEYWORDS = Object.freeze(['flee', 'run', 'hide', 'avoid', 'ignore', 'leave', 'walk away', 'back away', 'retreat', 'skip']);
|
|
109
|
+
const RISKY_KEYWORDS = Object.freeze(['reach', 'grab', 'fight', 'attack', 'steal', 'open', 'touch', 'eat', 'drink']);
|
|
110
|
+
const ADVENTURE_PREFERRED_TYPES = Object.freeze(['space', 'out west']);
|
|
107
111
|
|
|
108
112
|
function pickSafeChoice(choices) {
|
|
109
113
|
if (choices.length === 0) return null;
|
|
@@ -114,7 +118,8 @@ function pickSafeChoice(choices) {
|
|
|
114
118
|
|
|
115
119
|
// Prefer safe keywords
|
|
116
120
|
for (let i = 0; i < labels.length; i++) {
|
|
117
|
-
for (
|
|
121
|
+
for (let k = 0; k < SAFE_KEYWORDS.length; k++) {
|
|
122
|
+
const kw = SAFE_KEYWORDS[k];
|
|
118
123
|
if (labels[i].includes(kw)) {
|
|
119
124
|
LOG.info(`[adventure] Picking safe option: "${choices[i].label}" (matched: ${kw})`);
|
|
120
125
|
return choices[i];
|
|
@@ -235,9 +240,11 @@ async function playAdventureRounds(channel, msg) {
|
|
|
235
240
|
|
|
236
241
|
// Parse rewards from Adventure Progress embed
|
|
237
242
|
let rewards = '-';
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
const embeds = current.embeds || [];
|
|
244
|
+
for (let ei = 0; ei < embeds.length; ei++) {
|
|
245
|
+
const fields = embeds[ei].fields || [];
|
|
246
|
+
for (let fi = 0; fi < fields.length; fi++) {
|
|
247
|
+
if (fields[fi].name === 'Rewards') rewards = fields[fi].value;
|
|
241
248
|
}
|
|
242
249
|
}
|
|
243
250
|
LOG.info(`[adventure] Rewards: ${rewards}`);
|
|
@@ -282,7 +289,7 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
|
|
|
282
289
|
// ── 1) Check cooldown via Unix timestamp <t:UNIX:t> ────────
|
|
283
290
|
// Format: "You can start another adventure at <t:1774415487:t> (<t:1774415487:R>)"
|
|
284
291
|
let cooldownSec = 0;
|
|
285
|
-
const tsMatch = text.match(
|
|
292
|
+
const tsMatch = text.match(RE_DISCORD_TIMESTAMP);
|
|
286
293
|
if (tsMatch) {
|
|
287
294
|
const unixTarget = parseInt(tsMatch[1]);
|
|
288
295
|
const nowUnix = Math.floor(Date.now() / 1000);
|
|
@@ -341,7 +348,9 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
|
|
|
341
348
|
let menuRowIdx = -1;
|
|
342
349
|
for (let i = 0; i < (response.components || []).length; i++) {
|
|
343
350
|
const row = response.components[i];
|
|
344
|
-
|
|
351
|
+
const rowComps = row.components || [];
|
|
352
|
+
for (let j = 0; j < rowComps.length; j++) {
|
|
353
|
+
const comp = rowComps[j];
|
|
345
354
|
if (comp.type === 'STRING_SELECT' || comp.type === 3) { menuRowIdx = i; break; }
|
|
346
355
|
}
|
|
347
356
|
if (menuRowIdx >= 0) break;
|
|
@@ -349,9 +358,8 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
|
|
|
349
358
|
|
|
350
359
|
const menu = response.components[menuRowIdx]?.components[0];
|
|
351
360
|
const options = menu?.options || [];
|
|
352
|
-
const PREFERRED = ['space', 'out west'];
|
|
353
361
|
const preferred = options.filter(o =>
|
|
354
|
-
|
|
362
|
+
ADVENTURE_PREFERRED_TYPES.some(kw => (o.label || '').toLowerCase().includes(kw) || (o.value || '').toLowerCase().includes(kw))
|
|
355
363
|
);
|
|
356
364
|
const pool = preferred.length > 0 ? preferred : options;
|
|
357
365
|
|
|
@@ -433,7 +441,7 @@ function buildResult(finalText, coins, interactions, rewards, msg) {
|
|
|
433
441
|
|
|
434
442
|
// 1) Best: Unix timestamp <t:UNIX:t> in final text or embed
|
|
435
443
|
const allText = msg ? getFullText(msg) : finalText;
|
|
436
|
-
const tsMatch = allText.match(
|
|
444
|
+
const tsMatch = allText.match(RE_DISCORD_TIMESTAMP);
|
|
437
445
|
if (tsMatch) {
|
|
438
446
|
const unixTarget = parseInt(tsMatch[1]);
|
|
439
447
|
const nowUnix = Math.floor(Date.now() / 1000);
|
|
@@ -443,10 +451,13 @@ function buildResult(finalText, coins, interactions, rewards, msg) {
|
|
|
443
451
|
|
|
444
452
|
// 2) Fallback: "Adventure again in X minutes" button label
|
|
445
453
|
if (!nextCooldownSec && msg) {
|
|
446
|
-
|
|
447
|
-
|
|
454
|
+
const rows = msg.components || [];
|
|
455
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
456
|
+
const comps = rows[ri].components || [];
|
|
457
|
+
for (let ci = 0; ci < comps.length; ci++) {
|
|
458
|
+
const comp = comps[ci];
|
|
448
459
|
const label = (comp.label || '').toLowerCase();
|
|
449
|
-
const btnMatch = label.match(
|
|
460
|
+
const btnMatch = label.match(RE_ADVENTURE_AGAIN_LABEL);
|
|
450
461
|
if (btnMatch) {
|
|
451
462
|
nextCooldownSec = parseInt(btnMatch[1]);
|
|
452
463
|
const unit = btnMatch[2].toLowerCase();
|
package/lib/commands/beg.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
const { LOG, c, getFullText, parseCoins, logMsg, isHoldTight, getHoldTightReason, sleep } = require('./utils');
|
|
7
7
|
|
|
8
|
+
const RE_NEWLINE = /\n/g;
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* @param {object} opts
|
|
10
12
|
* @param {object} opts.channel
|
|
@@ -38,7 +40,7 @@ async function runBeg({ channel, waitForDankMemer }) {
|
|
|
38
40
|
return { result: `beg → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`, coins };
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
LOG.info(`[beg] ${text.substring(0, 80).replace(
|
|
43
|
+
LOG.info(`[beg] ${text.substring(0, 80).replace(RE_NEWLINE, ' ')}`);
|
|
42
44
|
return { result: text.substring(0, 60) || 'done', coins: 0 };
|
|
43
45
|
}
|
|
44
46
|
|
|
@@ -8,35 +8,50 @@ const {
|
|
|
8
8
|
logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, ensureCV2,
|
|
9
9
|
} = require('./utils');
|
|
10
10
|
|
|
11
|
+
const RE_BACKTICK_SCORE = /`\s*(\d+)\s*`/;
|
|
12
|
+
const RE_BJ_FACE_GLOBAL = /bjFace(\w+?):/g;
|
|
13
|
+
const RE_BJ_FACE_SUFFIX = /[RB]$/;
|
|
14
|
+
const RE_NET_LINE = /Net:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([+-]?[\d,]+)/i;
|
|
15
|
+
const RE_WINNINGS_LINE = /Winnings:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([\d,]+)/i;
|
|
16
|
+
const RE_COMMA = /,/g;
|
|
17
|
+
const COURT_RANKS = Object.freeze(['K', 'Q', 'J']);
|
|
18
|
+
|
|
11
19
|
function parsePlayerTotal(msg) {
|
|
12
20
|
// Score is in embed field: ` 18 ` (backtick-wrapped number in Player field)
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
const embeds = msg?.embeds || [];
|
|
22
|
+
for (let ei = 0; ei < embeds.length; ei++) {
|
|
23
|
+
const fields = embeds[ei].fields || [];
|
|
24
|
+
for (let fi = 0; fi < fields.length; fi++) {
|
|
25
|
+
const field = fields[fi];
|
|
15
26
|
if (field.name?.toLowerCase().includes('player')) {
|
|
16
|
-
const m = field.value.match(
|
|
27
|
+
const m = field.value.match(RE_BACKTICK_SCORE);
|
|
17
28
|
if (m) return parseInt(m[1]);
|
|
18
29
|
}
|
|
19
30
|
}
|
|
20
31
|
}
|
|
21
32
|
const text = typeof msg === 'string' ? msg : getFullText(msg);
|
|
22
|
-
const m = text.match(
|
|
33
|
+
const m = text.match(RE_BACKTICK_SCORE);
|
|
23
34
|
return m ? parseInt(m[1]) : 0;
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
function parseDealerUpcard(msg) {
|
|
27
38
|
// Dealer field: revealed total ` 18 ` or hidden ` ? `
|
|
28
39
|
// Upcard from emoji: bjFaceKB=K(10), bjFaceAR=A(11), bjFace7R=7
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
const embeds = msg?.embeds || [];
|
|
41
|
+
for (let ei = 0; ei < embeds.length; ei++) {
|
|
42
|
+
const fields = embeds[ei].fields || [];
|
|
43
|
+
for (let fi = 0; fi < fields.length; fi++) {
|
|
44
|
+
const field = fields[fi];
|
|
31
45
|
if (field.name?.toLowerCase().includes('dealer')) {
|
|
32
|
-
const totalMatch = field.value.match(
|
|
46
|
+
const totalMatch = field.value.match(RE_BACKTICK_SCORE);
|
|
33
47
|
if (totalMatch) return parseInt(totalMatch[1]);
|
|
34
|
-
const faces = [...field.value.matchAll(
|
|
35
|
-
for (
|
|
48
|
+
const faces = [...field.value.matchAll(RE_BJ_FACE_GLOBAL)];
|
|
49
|
+
for (let fii = 0; fii < faces.length; fii++) {
|
|
50
|
+
const face = faces[fii][1];
|
|
36
51
|
if (face === 'Unknown') continue;
|
|
37
|
-
const v = face.replace(
|
|
52
|
+
const v = face.replace(RE_BJ_FACE_SUFFIX, '');
|
|
38
53
|
if (v === 'A') return 11;
|
|
39
|
-
if (
|
|
54
|
+
if (COURT_RANKS.includes(v)) return 10;
|
|
40
55
|
const n = parseInt(v);
|
|
41
56
|
if (!isNaN(n)) return n;
|
|
42
57
|
}
|
|
@@ -47,10 +62,10 @@ function parseDealerUpcard(msg) {
|
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
function parseNetCoins(text) {
|
|
50
|
-
const netMatch = text.match(
|
|
51
|
-
if (netMatch) return parseInt(netMatch[1].replace(
|
|
52
|
-
const winMatch = text.match(
|
|
53
|
-
if (winMatch) return parseInt(winMatch[1].replace(
|
|
65
|
+
const netMatch = text.match(RE_NET_LINE);
|
|
66
|
+
if (netMatch) return parseInt(netMatch[1].replace(RE_COMMA, ''));
|
|
67
|
+
const winMatch = text.match(RE_WINNINGS_LINE);
|
|
68
|
+
if (winMatch) return parseInt(winMatch[1].replace(RE_COMMA, ''));
|
|
54
69
|
return 0;
|
|
55
70
|
}
|
|
56
71
|
|
package/lib/commands/crime.js
CHANGED
|
@@ -15,17 +15,21 @@ const {
|
|
|
15
15
|
} = require('./utils');
|
|
16
16
|
const { Trie, VoseAlias, LRUCache } = require('../structures');
|
|
17
17
|
|
|
18
|
-
const SAFE_CRIME_OPTIONS = [
|
|
18
|
+
const SAFE_CRIME_OPTIONS = Object.freeze([
|
|
19
19
|
'tax evasion', 'fraud', 'cybercrime', 'hacking', 'identity theft',
|
|
20
20
|
'money laundering', 'tax fraud', 'insurance fraud', 'scam',
|
|
21
|
-
];
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const RISKY_CRIME_OPTIONS = Object.freeze([
|
|
24
|
+
'murder', 'arson', 'assault', 'kidnap', 'terrorism',
|
|
25
|
+
]);
|
|
22
26
|
|
|
23
27
|
// Trie for O(k) safe option matching (replaces O(n) Set iteration)
|
|
24
28
|
const safeCrimeTrie = new Trie();
|
|
25
29
|
for (const opt of SAFE_CRIME_OPTIONS) safeCrimeTrie.insert(opt, opt);
|
|
26
30
|
|
|
27
31
|
const riskyCrimeTrie = new Trie();
|
|
28
|
-
for (const opt of
|
|
32
|
+
for (const opt of RISKY_CRIME_OPTIONS) {
|
|
29
33
|
riskyCrimeTrie.insert(opt, opt);
|
|
30
34
|
}
|
|
31
35
|
|
package/lib/commands/drops.js
CHANGED
|
@@ -3,6 +3,16 @@ const {
|
|
|
3
3
|
safeClickButton, logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
|
|
4
4
|
} = require('./utils');
|
|
5
5
|
|
|
6
|
+
const RE_BLOCK_SPLIT = /\n\n+/;
|
|
7
|
+
const RE_DROP_NAME_BOLD = /\*\*(.+?)\*\*/;
|
|
8
|
+
const RE_COST_EMBED = /Cost:\s*[⏣💰]?\s*([\d,]+)/i;
|
|
9
|
+
const RE_STOCK = /Stock:\s*(\d+|Infinite)/i;
|
|
10
|
+
const RE_DROP_TIMESTAMP = /<t:(\d+):[tTdDfFR]>/;
|
|
11
|
+
const RE_COMMA = /,/g;
|
|
12
|
+
const RE_DROP_NAME_FALLBACK = /(\w[\w\s']+)/;
|
|
13
|
+
const RE_COST_LINE = /Cost:\s*[⏣💰o]?\s*([\d,]+)/i;
|
|
14
|
+
const RE_WHITESPACE_UNDERSCORE = /\s+/g;
|
|
15
|
+
|
|
6
16
|
async function runDrops({ channel, waitForDankMemer, redis, accountId }) {
|
|
7
17
|
LOG.cmd(`${c.white}${c.bold}pls drops${c.reset}`);
|
|
8
18
|
|
|
@@ -22,18 +32,21 @@ async function runDrops({ channel, waitForDankMemer, redis, accountId }) {
|
|
|
22
32
|
|
|
23
33
|
const drops = [];
|
|
24
34
|
|
|
25
|
-
|
|
35
|
+
const embedList = response.embeds || [];
|
|
36
|
+
for (let ei = 0; ei < embedList.length; ei++) {
|
|
37
|
+
const embed = embedList[ei];
|
|
26
38
|
const desc = embed.description || '';
|
|
27
|
-
const items = desc.split(
|
|
28
|
-
for (
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
39
|
+
const items = desc.split(RE_BLOCK_SPLIT);
|
|
40
|
+
for (let bi = 0; bi < items.length; bi++) {
|
|
41
|
+
const block = items[bi];
|
|
42
|
+
const nameMatch = block.match(RE_DROP_NAME_BOLD);
|
|
43
|
+
const costMatch = block.match(RE_COST_EMBED);
|
|
44
|
+
const stockMatch = block.match(RE_STOCK);
|
|
45
|
+
const dateMatch = block.match(RE_DROP_TIMESTAMP);
|
|
33
46
|
if (nameMatch) {
|
|
34
47
|
drops.push({
|
|
35
48
|
name: nameMatch[1],
|
|
36
|
-
cost: costMatch ? parseInt(costMatch[1].replace(
|
|
49
|
+
cost: costMatch ? parseInt(costMatch[1].replace(RE_COMMA, '')) : 0,
|
|
37
50
|
stock: stockMatch ? (stockMatch[1] === 'Infinite' ? Infinity : parseInt(stockMatch[1])) : 0,
|
|
38
51
|
dropTimestamp: dateMatch ? parseInt(dateMatch[1]) : 0,
|
|
39
52
|
});
|
|
@@ -47,10 +60,10 @@ async function runDrops({ channel, waitForDankMemer, redis, accountId }) {
|
|
|
47
60
|
const line = textBlocks[i];
|
|
48
61
|
if (line.includes('Cost:') && i > 0) {
|
|
49
62
|
const prevLine = textBlocks[i - 1];
|
|
50
|
-
const nameMatch = prevLine.match(
|
|
51
|
-
const costMatch = line.match(
|
|
63
|
+
const nameMatch = prevLine.match(RE_DROP_NAME_BOLD) || prevLine.match(RE_DROP_NAME_FALLBACK);
|
|
64
|
+
const costMatch = line.match(RE_COST_LINE);
|
|
52
65
|
if (nameMatch && costMatch) {
|
|
53
|
-
const cost = parseInt(costMatch[1].replace(
|
|
66
|
+
const cost = parseInt(costMatch[1].replace(RE_COMMA, ''));
|
|
54
67
|
const existing = drops.find(d => d.name === nameMatch[1]);
|
|
55
68
|
if (!existing && cost > 0) {
|
|
56
69
|
drops.push({ name: nameMatch[1], cost, stock: 0, dropTimestamp: 0 });
|
|
@@ -64,9 +77,10 @@ async function runDrops({ channel, waitForDankMemer, redis, accountId }) {
|
|
|
64
77
|
try {
|
|
65
78
|
await redis.set('dkg:drops:latest', JSON.stringify(drops), 'EX', 172800);
|
|
66
79
|
// Schedule individual drops with their timestamps
|
|
67
|
-
for (
|
|
80
|
+
for (let di = 0; di < drops.length; di++) {
|
|
81
|
+
const drop = drops[di];
|
|
68
82
|
if (drop.dropTimestamp > 0) {
|
|
69
|
-
const key = `dkg:drops:schedule:${drop.name.replace(
|
|
83
|
+
const key = `dkg:drops:schedule:${drop.name.replace(RE_WHITESPACE_UNDERSCORE, '_').toLowerCase()}`;
|
|
70
84
|
await redis.set(key, JSON.stringify(drop), 'EX', 172800);
|
|
71
85
|
}
|
|
72
86
|
}
|
|
@@ -77,7 +91,8 @@ async function runDrops({ channel, waitForDankMemer, redis, accountId }) {
|
|
|
77
91
|
const now = Math.floor(Date.now() / 1000);
|
|
78
92
|
const buttons = getAllButtons(response);
|
|
79
93
|
let boughtCount = 0;
|
|
80
|
-
for (
|
|
94
|
+
for (let di = 0; di < drops.length; di++) {
|
|
95
|
+
const drop = drops[di];
|
|
81
96
|
if (drop.stock > 0 && (drop.dropTimestamp === 0 || drop.dropTimestamp <= now)) {
|
|
82
97
|
const buyBtn = buttons.find(b =>
|
|
83
98
|
!b.disabled && b.label && (
|
package/lib/commands/fish.js
CHANGED
|
@@ -22,6 +22,11 @@ const { downloadImage, extractImageUrl, findSafeCells } = require('./fishVision'
|
|
|
22
22
|
|
|
23
23
|
const { DANK_MEMER_ID } = require('./utils');
|
|
24
24
|
|
|
25
|
+
const RE_FISH_COOLDOWN_TS = /<t:(\d+):R>/;
|
|
26
|
+
const RE_FISH_HEADER = /###\s*(.+?)(?:\s*-#|$)/;
|
|
27
|
+
const RE_CAUGHT_PHRASE = /caught\s+(?:a\s+)?(.+?)(?:\s*[!.]|$)/i;
|
|
28
|
+
const RE_CATCH_CUSTOM_ID_COORDS = /:(\d+):(\d+)$/;
|
|
29
|
+
|
|
25
30
|
async function refetchMsg(channel, msgId) {
|
|
26
31
|
try { return await channel.messages.fetch(msgId); } catch { return null; }
|
|
27
32
|
}
|
|
@@ -127,7 +132,7 @@ async function sellAllFish({ channel, waitForDankMemer, sellFor = 'tokens' }) {
|
|
|
127
132
|
* Parse cooldown timestamp from text.
|
|
128
133
|
*/
|
|
129
134
|
function parseFishCooldown(text) {
|
|
130
|
-
const m = text.match(
|
|
135
|
+
const m = text.match(RE_FISH_COOLDOWN_TS);
|
|
131
136
|
if (m) {
|
|
132
137
|
const diff = parseInt(m[1]) - Math.floor(Date.now() / 1000);
|
|
133
138
|
return diff > 0 ? diff : null;
|
|
@@ -194,7 +199,7 @@ async function waitForResult({ channel, msgId, waitForDankMemer }) {
|
|
|
194
199
|
function parseFishResult(text) {
|
|
195
200
|
const tl = text.toLowerCase();
|
|
196
201
|
const cd = parseFishCooldown(text);
|
|
197
|
-
const headerMatch = text.match(
|
|
202
|
+
const headerMatch = text.match(RE_FISH_HEADER);
|
|
198
203
|
const header = headerMatch ? headerMatch[1].trim() : '';
|
|
199
204
|
|
|
200
205
|
if (tl.includes('nothing to catch') || tl.includes('no fish')) {
|
|
@@ -207,7 +212,7 @@ function parseFishResult(text) {
|
|
|
207
212
|
return { outcome: 'got_away', header, cd };
|
|
208
213
|
}
|
|
209
214
|
if (tl.includes('caught')) {
|
|
210
|
-
const fishMatch = text.match(
|
|
215
|
+
const fishMatch = text.match(RE_CAUGHT_PHRASE);
|
|
211
216
|
return { outcome: 'caught', fish: fishMatch ? fishMatch[1].substring(0, 40) : 'a fish', header, cd };
|
|
212
217
|
}
|
|
213
218
|
return { outcome: 'unknown', header: header || text.substring(0, 60), cd };
|
|
@@ -263,7 +268,7 @@ async function playFishRound({ gameMsg, channel, msgId, waitForDankMemer }) {
|
|
|
263
268
|
|
|
264
269
|
const catchBtns = getAllButtons(gameMsg).filter(b => (b.label || '').toLowerCase() === 'catch' && !b.disabled);
|
|
265
270
|
const btn = catchBtns.find(b => {
|
|
266
|
-
const m = (b.customId || '').match(
|
|
271
|
+
const m = (b.customId || '').match(RE_CATCH_CUSTOM_ID_COORDS);
|
|
267
272
|
return m && parseInt(m[1]) === target.col && parseInt(m[2]) === target.row;
|
|
268
273
|
});
|
|
269
274
|
|
package/lib/commands/gamble.js
CHANGED
|
@@ -87,9 +87,12 @@ function parseResult(text, cmdName) {
|
|
|
87
87
|
return { result: `${cmdName} done`, coins: 0 };
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
const RE_LESS_THAN = /less than\s*\*?\*?[⏣o]?\s*([\d,]+)/i;
|
|
91
|
+
const RE_FIRST_NUM = /(\d[\d,]+)/;
|
|
92
|
+
|
|
90
93
|
function checkMinBet(text) {
|
|
91
94
|
if (!minBetDetector.hasAny(text)) return 0;
|
|
92
|
-
const m = text.match(
|
|
95
|
+
const m = text.match(RE_LESS_THAN) || text.match(RE_FIRST_NUM);
|
|
93
96
|
if (m) return parseInt(m[1].replace(/,/g, ''));
|
|
94
97
|
return -1;
|
|
95
98
|
}
|
package/lib/commands/highlow.js
CHANGED
|
@@ -8,18 +8,26 @@ const {
|
|
|
8
8
|
logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
|
|
9
9
|
} = require('./utils');
|
|
10
10
|
|
|
11
|
+
// Pre-compiled regex — avoid V8 recompilation on every call
|
|
12
|
+
const RE_HINT_BOLD = /hint.*?\*\*(\d+)\*\*/i;
|
|
13
|
+
const RE_NUMBER_BOLD = /number.*?\*\*(\d+)\*\*/i;
|
|
14
|
+
const RE_HINT_PLAIN = /hint.*?(\d+)/i;
|
|
15
|
+
const RE_NUMBER_PLAIN = /number.*?(\d+)/i;
|
|
16
|
+
const RE_STANDALONE_NUM = /\b(\d{1,3})\b/g;
|
|
17
|
+
const RE_HL_NET = /Net:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([+-]?[\d,]+)/i;
|
|
18
|
+
const RE_HL_WIN = /Winnings:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([\d,]+)/i;
|
|
19
|
+
|
|
11
20
|
function parseHintNumber(text) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|| text.match(
|
|
15
|
-
|| text.match(
|
|
16
|
-
|| text.match(/number.*?(\d+)/i);
|
|
21
|
+
const hintMatch = text.match(RE_HINT_BOLD)
|
|
22
|
+
|| text.match(RE_NUMBER_BOLD)
|
|
23
|
+
|| text.match(RE_HINT_PLAIN)
|
|
24
|
+
|| text.match(RE_NUMBER_PLAIN);
|
|
17
25
|
if (hintMatch) return parseInt(hintMatch[1]);
|
|
18
|
-
|
|
19
|
-
const nums = text.match(
|
|
26
|
+
RE_STANDALONE_NUM.lastIndex = 0;
|
|
27
|
+
const nums = text.match(RE_STANDALONE_NUM);
|
|
20
28
|
if (nums) {
|
|
21
|
-
for (
|
|
22
|
-
const v = parseInt(
|
|
29
|
+
for (let i = 0; i < nums.length; i++) {
|
|
30
|
+
const v = parseInt(nums[i]);
|
|
23
31
|
if (v >= 1 && v <= 100) return v;
|
|
24
32
|
}
|
|
25
33
|
}
|
|
@@ -27,9 +35,9 @@ function parseHintNumber(text) {
|
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
function parseNetCoins(text) {
|
|
30
|
-
const netMatch = text.match(
|
|
38
|
+
const netMatch = text.match(RE_HL_NET);
|
|
31
39
|
if (netMatch) return parseInt(netMatch[1].replace(/,/g, ''));
|
|
32
|
-
const winMatch = text.match(
|
|
40
|
+
const winMatch = text.match(RE_HL_WIN);
|
|
33
41
|
if (winMatch) return parseInt(winMatch[1].replace(/,/g, ''));
|
|
34
42
|
return 0;
|
|
35
43
|
}
|
|
@@ -18,6 +18,8 @@ const {
|
|
|
18
18
|
sleep, humanDelay,
|
|
19
19
|
} = require('./utils');
|
|
20
20
|
|
|
21
|
+
const RE_COOLDOWN_MIN = /(\d+)\s*minute/i;
|
|
22
|
+
|
|
21
23
|
/**
|
|
22
24
|
* Re-fetch a message to get updated state after an interaction.
|
|
23
25
|
*/
|
|
@@ -60,7 +62,7 @@ async function runPostMemes({ channel, waitForDankMemer }) {
|
|
|
60
62
|
// Detect "dead meme" / cooldown message → return with nextCooldownSec
|
|
61
63
|
if (initLower.includes('cannot post another meme') || initLower.includes('dead meme') ||
|
|
62
64
|
initLower.includes('another meme for another')) {
|
|
63
|
-
const minMatch = initText.match(
|
|
65
|
+
const minMatch = initText.match(RE_COOLDOWN_MIN);
|
|
64
66
|
const cdSec = minMatch ? parseInt(minMatch[1]) * 60 : 120;
|
|
65
67
|
LOG.warn(`[pm] Cooldown: ${cdSec}s`);
|
|
66
68
|
return { result: `pm cooldown ${cdSec}s`, coins: 0, nextCooldownSec: cdSec };
|
package/lib/commands/profile.js
CHANGED
|
@@ -12,16 +12,17 @@ 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*(\d+)/i;
|
|
16
|
+
const RE_PROFILE_PRESTIGE_LEVEL = /prestige\s+\d+\s+level\s+(\d+)/i;
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
19
|
* Parse level from profile response text.
|
|
17
20
|
* Looks for patterns like "Level 25", "Lvl 25", "level: 25"
|
|
18
21
|
*/
|
|
19
22
|
function parseLevelFromText(text) {
|
|
20
|
-
|
|
21
|
-
const match = text.match(/(?:level|lvl)\s*:?\s*(\d+)/i);
|
|
23
|
+
const match = text.match(RE_PROFILE_LEVEL);
|
|
22
24
|
if (match) return parseInt(match[1]);
|
|
23
|
-
|
|
24
|
-
const presMatch = text.match(/prestige\s+\d+\s+level\s+(\d+)/i);
|
|
25
|
+
const presMatch = text.match(RE_PROFILE_PRESTIGE_LEVEL);
|
|
25
26
|
if (presMatch) return parseInt(presMatch[1]);
|
|
26
27
|
return null;
|
|
27
28
|
}
|
package/lib/commands/search.js
CHANGED
|
@@ -15,12 +15,12 @@ const {
|
|
|
15
15
|
} = require('./utils');
|
|
16
16
|
const { VoseAlias, Trie, EMA, LRUCache } = require('../structures');
|
|
17
17
|
|
|
18
|
-
const SAFE_SEARCH_LOCATIONS = [
|
|
18
|
+
const SAFE_SEARCH_LOCATIONS = Object.freeze([
|
|
19
19
|
'sofa', 'mailbox', 'dog', 'car', 'dresser', 'laundromat', 'bed',
|
|
20
20
|
'couch', 'pantry', 'fridge', 'kitchen', 'bathroom', 'attic',
|
|
21
21
|
'closet', 'shoe', 'vacuum', 'toilet', 'sink', 'shower',
|
|
22
22
|
'tree', 'grass', 'bushes', 'garden', 'park', 'backyard',
|
|
23
|
-
];
|
|
23
|
+
]);
|
|
24
24
|
|
|
25
25
|
// Trie: O(k) lookup for safe locations — replaces O(n) Set iteration per button
|
|
26
26
|
const safeTrie = new Trie();
|
package/lib/commands/stream.js
CHANGED
|
@@ -4,7 +4,7 @@ const {
|
|
|
4
4
|
} = require('./utils');
|
|
5
5
|
const { buyItem } = require('./shop');
|
|
6
6
|
|
|
7
|
-
const STREAM_ITEMS = ['keyboard', 'mouse'];
|
|
7
|
+
const STREAM_ITEMS = Object.freeze(['keyboard', 'mouse']);
|
|
8
8
|
|
|
9
9
|
async function runStream({ channel, waitForDankMemer, client }) {
|
|
10
10
|
LOG.cmd(`${c.white}${c.bold}pls stream${c.reset}`);
|
package/lib/commands/trivia.js
CHANGED
|
@@ -12,6 +12,10 @@ const {
|
|
|
12
12
|
logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
|
|
13
13
|
} = require('./utils');
|
|
14
14
|
|
|
15
|
+
const RE_TRIVIA_MD_BOLD = /\*\*/g;
|
|
16
|
+
const RE_TRIVIA_MD_ITALIC = /\*.*?\*/g;
|
|
17
|
+
const RE_TRIVIA_CORRECT_WAS = /correct answer was \*\*(.+?)\*\*/i;
|
|
18
|
+
|
|
15
19
|
const triviaDB = {};
|
|
16
20
|
|
|
17
21
|
function makeQuestionKey(question) {
|
|
@@ -101,7 +105,7 @@ async function runTrivia({ channel, waitForDankMemer, redis }) {
|
|
|
101
105
|
const clickable = buttons.filter(b => !b.disabled);
|
|
102
106
|
if (clickable.length === 0) return { result: 'no clickable buttons', coins: 0 };
|
|
103
107
|
|
|
104
|
-
const question = (response.embeds?.[0]?.description || '').replace(
|
|
108
|
+
const question = (response.embeds?.[0]?.description || '').replace(RE_TRIVIA_MD_BOLD, '').replace(RE_TRIVIA_MD_ITALIC, '').trim();
|
|
105
109
|
|
|
106
110
|
let btn = await lookupAnswer(question, clickable, redis || null);
|
|
107
111
|
const fromDB = !!btn;
|
|
@@ -119,7 +123,7 @@ async function runTrivia({ channel, waitForDankMemer, redis }) {
|
|
|
119
123
|
const coins = parseCoins(text);
|
|
120
124
|
|
|
121
125
|
const lower = text.toLowerCase();
|
|
122
|
-
const correctMatch = text.match(
|
|
126
|
+
const correctMatch = text.match(RE_TRIVIA_CORRECT_WAS);
|
|
123
127
|
if (correctMatch) {
|
|
124
128
|
await learnFromResult(question, correctMatch[1], redis || null);
|
|
125
129
|
} else if (lower.includes('correct') || lower.includes('nice') || lower.includes('right')) {
|