dankgrinder 8.109.0 → 8.111.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.
@@ -37,6 +37,7 @@ const {
37
37
  LOG, c, sleep, humanDelay, getFullText, parseCoins,
38
38
  getAllButtons, getAllSelectMenus, findButton,
39
39
  safeClickButton, isHoldTight, logMsg, checkLevelLock,
40
+ sendCommand,
40
41
  } = require('./utils');
41
42
 
42
43
  const RE_DISCORD_TIMESTAMP = /<t:(\d+)(?::[tTdDfFR])?>/g;
@@ -108,14 +109,31 @@ const SAFE_KEYWORDS = Object.freeze(['flee', 'run', 'hide', 'avoid', 'ignore', '
108
109
  const RISKY_KEYWORDS = Object.freeze(['reach', 'grab', 'fight', 'attack', 'steal', 'open', 'touch', 'eat', 'drink']);
109
110
  const ADVENTURE_PREFERRED_TYPES = Object.freeze(['space', 'out west']);
110
111
 
111
- function pickSafeChoice(choices) {
112
+ function pickSafeChoice(choices, adventureAnswers) {
112
113
  if (choices.length === 0) return null;
113
114
  if (choices.length === 1) return choices[0];
114
115
 
115
- // Check labels for safe/risky keywords
116
116
  const labels = choices.map(b => (b.label || '').toLowerCase());
117
117
 
118
- // Prefer safe keywords
118
+ // ── 1) Config answer maps (dmg pattern — highest priority) ──
119
+ if (adventureAnswers) {
120
+ for (const [question, answers] of Object.entries(adventureAnswers)) {
121
+ const matched = labels.find(l => l.includes(question));
122
+ if (matched && Array.isArray(answers)) {
123
+ for (const answer of answers) {
124
+ const found = choices.find(c =>
125
+ (c.label || '').toLowerCase().includes(answer.toLowerCase())
126
+ );
127
+ if (found) {
128
+ LOG.info(`[adventure] Config answer: "${found.label}" matched question "${question}"`);
129
+ return found;
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ // ── 2) Hardcoded safe keywords ──
119
137
  for (let i = 0; i < labels.length; i++) {
120
138
  for (let k = 0; k < SAFE_KEYWORDS.length; k++) {
121
139
  const kw = SAFE_KEYWORDS[k];
@@ -126,7 +144,7 @@ function pickSafeChoice(choices) {
126
144
  }
127
145
  }
128
146
 
129
- // Avoid risky keywords
147
+ // ── 3) Avoid risky keywords ──
130
148
  const nonRisky = choices.filter((b, i) => {
131
149
  return !RISKY_KEYWORDS.some(kw => labels[i].includes(kw));
132
150
  });
@@ -143,7 +161,7 @@ function pickSafeChoice(choices) {
143
161
  }
144
162
 
145
163
  // ── Play through all adventure rounds ────────────────────────────
146
- async function playAdventureRounds(channel, msg) {
164
+ async function playAdventureRounds(channel, msg, adventureAnswers) {
147
165
  let current = msg;
148
166
  let interactions = 0;
149
167
  const MAX_INTERACTIONS = 30;
@@ -171,7 +189,7 @@ async function playAdventureRounds(channel, msg) {
171
189
  // ── If there are choices, pick one first ─────────────────
172
190
  if (choices.length > 0) {
173
191
  LOG.info(`[adventure] ${choices.length} choices: [${choices.map(b => `"${b.label}"`).join(', ')}]`);
174
- const choice = pickSafeChoice(choices);
192
+ const choice = pickSafeChoice(choices, adventureAnswers);
175
193
  if (choice) {
176
194
  LOG.info(`[adventure] → Choosing: "${choice.label}"`);
177
195
  await sleep(50);
@@ -258,13 +276,16 @@ async function playAdventureRounds(channel, msg) {
258
276
  * @param {object} opts.channel - Discord channel
259
277
  * @param {function} opts.waitForDankMemer - Waits for Dank Memer response
260
278
  * @param {object} [opts.client] - Discord client (for modal handling in shop)
279
+ * @param {object} [opts.adventureAnswers] - Per-type answer map from AccountWorker.ADVENTURE_ANSWERS
280
+ * @param {boolean} [opts.useSlash] - Use Discord slash command API instead of text
281
+ * @param {string} [opts.prefix] - Text prefix ('pls' or '/')
261
282
  * @returns {Promise<{result: string, coins: number, nextCooldownSec: number|null}>}
262
283
  */
263
- async function runAdventure({ channel, waitForDankMemer, client }) {
284
+ async function runAdventure({ channel, waitForDankMemer, client, adventureAnswers, useSlash, prefix = 'pls' }) {
264
285
  LOG.cmd(`${c.white}${c.bold}pls adventure${c.reset}`);
265
286
 
266
287
  // Step 1: Send the command
267
- await channel.send('pls adventure');
288
+ await sendCommand(channel, 'adventure', { useSlash, prefix });
268
289
  let response = await waitForDankMemer(12000);
269
290
 
270
291
  if (!response) {
@@ -334,7 +355,7 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
334
355
  if (hasNextBtn && menus.length === 0) {
335
356
  // Already mid-adventure from a previous run — jump straight to rounds
336
357
  LOG.info('[adventure] Resuming mid-adventure...');
337
- const { text: finalText, coins, interactions, rewards, finalMsg } = await playAdventureRounds(channel, response);
358
+ const { text: finalText, coins, interactions, rewards, finalMsg } = await playAdventureRounds(channel, response, adventureAnswers);
338
359
  return buildResult(finalText, coins, interactions, rewards, finalMsg);
339
360
  }
340
361
 
@@ -434,7 +455,7 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
434
455
  }
435
456
 
436
457
  // ── Play through all adventure rounds ──────────────────────
437
- const { text: finalText, coins, interactions, rewards, finalMsg } = await playAdventureRounds(channel, response);
458
+ const { text: finalText, coins, interactions, rewards, finalMsg } = await playAdventureRounds(channel, response, adventureAnswers);
438
459
  return buildResult(finalText, coins, interactions, rewards, finalMsg);
439
460
  }
440
461
 
@@ -3,7 +3,8 @@
3
3
  * Simple command: send "pls beg", parse coins from response.
4
4
  */
5
5
 
6
- const { LOG, c, getFullText, parseCoins, logMsg, isHoldTight, getHoldTightReason, sleep, checkLevelLock } = require('./utils');
6
+ const { LOG, c, getFullText, parseCoins, logMsg, isHoldTight, getHoldTightReason, sleep, checkLevelLock,
7
+ sendCommand, } = require('./utils');
7
8
 
8
9
  const RE_NEWLINE = /\n/g;
9
10
 
@@ -16,7 +17,7 @@ const RE_NEWLINE = /\n/g;
16
17
  async function runBeg({ channel, waitForDankMemer }) {
17
18
  LOG.cmd(`${c.white}${c.bold}pls beg${c.reset}`);
18
19
 
19
- await channel.send('pls beg');
20
+ await sendCommand(channel, 'beg', { useSlash, prefix });
20
21
  const response = await waitForDankMemer(10000);
21
22
 
22
23
  if (!response) {
@@ -7,6 +7,7 @@
7
7
  const {
8
8
  LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
9
9
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, ensureCV2, checkLevelLock,
10
+ sendCommand,
10
11
  } = require('./utils');
11
12
 
12
13
  const RE_BACKTICK_SCORE = /`\s*(\d+)\s*`/;
@@ -133,11 +134,11 @@ async function clickFast(msg, btn, retries = 2) {
133
134
  return null;
134
135
  }
135
136
 
136
- async function runBlackjack({ channel, waitForDankMemer, betAmount = 5000 }) {
137
- const cmd = `pls bj ${betAmount}`;
137
+ async function runBlackjack({ channel, waitForDankMemer, betAmount = 5000, useSlash, prefix = 'pls' }) {
138
+ const cmdName = 'bj';
139
+ const cmd = `${prefix} ${cmdName} ${betAmount}`;
138
140
  LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
139
-
140
- await channel.send(cmd);
141
+ await sendCommand(channel, cmdName, { useSlash, prefix, args: [betAmount] });
141
142
  let current = await waitForDankMemer(12000);
142
143
 
143
144
  if (!current) {
@@ -12,6 +12,7 @@ const {
12
12
  LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
13
13
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
14
14
  isCV2, ensureCV2, checkLevelLock,
15
+ sendCommand,
15
16
  } = require('./utils');
16
17
  const { Trie, VoseAlias, LRUCache } = require('../structures');
17
18
 
@@ -86,7 +87,7 @@ function pickSafeButton(buttons, customSafe) {
86
87
  async function runCrime({ channel, waitForDankMemer, safeAnswers }) {
87
88
  LOG.cmd(`${c.white}${c.bold}pls crime${c.reset}`);
88
89
 
89
- await channel.send('pls crime');
90
+ await sendCommand(channel, 'crime', { useSlash, prefix });
90
91
  const response = await waitForDankMemer(10000);
91
92
 
92
93
  if (!response) {
@@ -5,6 +5,7 @@
5
5
 
6
6
  const {
7
7
  LOG, c, getFullText, parseCoins, logMsg, isHoldTight, getHoldTightReason, sleep,
8
+ sendCommand,
8
9
  } = require('./utils');
9
10
 
10
11
  /**
@@ -16,7 +17,7 @@ const {
16
17
  async function runDeposit({ channel, waitForDankMemer }) {
17
18
  LOG.cmd(`${c.white}${c.bold}pls dep max${c.reset}`);
18
19
 
19
- await channel.send('pls dep max');
20
+ await sendCommand(channel, 'dep max', { useSlash, prefix });
20
21
  const response = await waitForDankMemer(10000);
21
22
 
22
23
  if (!response) {
@@ -7,6 +7,7 @@ const {
7
7
  LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton, humanDelay,
8
8
  logMsg, isHoldTight, getHoldTightReason, sleep, needsItem,
9
9
  isCV2, ensureCV2, stripAnsi, checkLevelLock,
10
+ sendCommand,
10
11
  } = require('./utils');
11
12
  const { buyItem } = require('./shop');
12
13
 
@@ -96,7 +97,7 @@ async function resolveDigFlow({ channel, waitForDankMemer, response }) {
96
97
  async function runDig({ channel, waitForDankMemer, client }) {
97
98
  LOG.cmd(`${c.white}${c.bold}pls dig${c.reset}`);
98
99
 
99
- await channel.send('pls dig');
100
+ await sendCommand(channel, 'dig', { useSlash, prefix });
100
101
  const response = await waitForDankMemer(10000);
101
102
 
102
103
  if (!response) {
@@ -142,7 +143,7 @@ async function runDig({ channel, waitForDankMemer, client }) {
142
143
  while (await waitForDankMemer(1500)) { /* drain */ }
143
144
  await sleep(1000);
144
145
 
145
- await channel.send('pls dig');
146
+ await sendCommand(channel, 'dig', { useSlash, prefix });
146
147
  const r2 = await waitForDankMemer(10000);
147
148
  if (r2) {
148
149
  if (isCV2(r2)) await ensureCV2(r2);
@@ -1,6 +1,7 @@
1
1
  const {
2
2
  LOG, c, getFullText, parseCoins, getAllButtons,
3
3
  safeClickButton, logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
4
+ sendCommand,
4
5
  } = require('./utils');
5
6
 
6
7
  const RE_BLOCK_SPLIT = /\n\n+/;
@@ -16,7 +17,7 @@ const RE_WHITESPACE_UNDERSCORE = /\s+/g;
16
17
  async function runDrops({ channel, waitForDankMemer, redis, accountId }) {
17
18
  LOG.cmd(`${c.white}${c.bold}pls drops${c.reset}`);
18
19
 
19
- await channel.send('pls drops');
20
+ await sendCommand(channel, 'drops', { useSlash, prefix });
20
21
  const response = await waitForDankMemer(10000);
21
22
 
22
23
  if (!response) return { result: 'no response', coins: 0 };
@@ -2,6 +2,7 @@ const {
2
2
  LOG, c, getFullText, parseCoins, getAllButtons, getAllSelectMenus,
3
3
  safeClickButton, logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
4
4
  isCV2, ensureCV2, stripAnsi, needsItem, clickCV2SelectMenu, checkLevelLock,
5
+ sendCommand,
5
6
  } = require('./utils');
6
7
  const { buyItem, buyItemsBatch } = require('./shop');
7
8
  const rawLogger = require('../../lib/rawLogger');
@@ -1295,7 +1296,7 @@ async function advancePastConfirmation(response, waitForDankMemer) {
1295
1296
  async function runFarm({ channel, waitForDankMemer, client, redis, accountId, forceRun }) {
1296
1297
  LOG.cmd(`${c.white}${c.bold}pls farm view${c.reset}`);
1297
1298
 
1298
- await channel.send('pls farm view');
1299
+ await sendCommand(channel, 'farm view', { useSlash, prefix });
1299
1300
  let response = await waitForDankMemer(12000);
1300
1301
 
1301
1302
  if (!response) {
@@ -1322,7 +1323,7 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
1322
1323
  // Subcommand required — retry once.
1323
1324
  if (lower.includes('must specify a subcommand')) {
1324
1325
  LOG.warn('[farm] Subcommand required; retrying "pls farm view"');
1325
- await channel.send('pls farm view');
1326
+ await sendCommand(channel, 'farm view', { useSlash, prefix });
1326
1327
  const retry = await waitForDankMemer(12000);
1327
1328
  if (!retry) return { result: 'no response after farm view retry', coins: 0, nextCooldownSec: 120 };
1328
1329
  response = retry;
@@ -1383,7 +1384,7 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
1383
1384
  LOG.success(`[farm] Auto-bought ${bought.itemName}. Retrying farm flow...`);
1384
1385
  await sleep(1200);
1385
1386
  // Retry once with the same flow.
1386
- await channel.send('pls farm view');
1387
+ await sendCommand(channel, 'farm view', { useSlash, prefix });
1387
1388
  response = await waitForDankMemer(12000);
1388
1389
  if (!response) return { result: 'no response after retry', coins: 0, nextCooldownSec: 90 };
1389
1390
  if (isCV2(response)) await ensureCV2(response);
@@ -1474,7 +1475,7 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId, fo
1474
1475
  LOG.success(`[farm] Bought ${missing}. Retrying cycle...`);
1475
1476
  await sleep(1200);
1476
1477
  // Restart the whole cycle after buying
1477
- await channel.send('pls farm view');
1478
+ await sendCommand(channel, 'farm view', { useSlash, prefix });
1478
1479
  const re = await waitForDankMemer(12000);
1479
1480
  if (!re) return { result: 'no response after restock retry', coins: 0, nextCooldownSec: 90 };
1480
1481
  if (isCV2(re)) await ensureCV2(re);
@@ -18,10 +18,12 @@ const {
18
18
  LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
19
19
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
20
20
  isCV2, ensureCV2, checkLevelLock,
21
+ sendCommand,
21
22
  } = require('./utils');
22
23
  const { downloadImage, extractImageUrl, findSafeCells } = require('./fishVision');
23
24
 
24
- const { DANK_MEMER_ID } = require('./utils');
25
+ const { DANK_MEMER_ID sendCommand,
26
+ } = require('./utils');
25
27
 
26
28
  const RE_FISH_COOLDOWN_TS = /<t:(\d+):R>/;
27
29
  const RE_FISH_HEADER = /###\s*(.+?)(?:\s*-#|$)/;
@@ -41,7 +43,7 @@ async function refetchMsg(channel, msgId) {
41
43
  */
42
44
  async function sellAllFish({ channel, waitForDankMemer, sellFor = 'tokens' }) {
43
45
  LOG.info('[fish] Selling all fish from buckets...');
44
- await channel.send('pls fish buckets');
46
+ await sendCommand(channel, 'fish buckets', { useSlash, prefix });
45
47
  let msg = await waitForDankMemer(8000);
46
48
  if (!msg) { LOG.warn('[fish] No response to pls fish buckets'); return false; }
47
49
  let fresh = await refetchMsg(channel, msg.id);
@@ -308,7 +310,7 @@ async function playFishRound({ gameMsg, channel, msgId, waitForDankMemer }) {
308
310
  async function runFish({ channel, waitForDankMemer }) {
309
311
  LOG.cmd(`${c.white}${c.bold}pls fish catch${c.reset}`);
310
312
 
311
- await channel.send('pls fish catch');
313
+ await sendCommand(channel, 'fish catch', { useSlash, prefix });
312
314
  let response = await waitForDankMemer(10000);
313
315
  if (!response) return { result: 'no response', coins: 0 };
314
316
 
@@ -393,7 +395,7 @@ async function runFish({ channel, waitForDankMemer }) {
393
395
  async function runFishLoop({ channel, waitForDankMemer, maxRounds = 50, onRound, signal }) {
394
396
  LOG.cmd(`${c.white}${c.bold}pls fish catch${c.reset} (loop mode, max ${maxRounds} rounds)`);
395
397
 
396
- await channel.send('pls fish catch');
398
+ await sendCommand(channel, 'fish catch', { useSlash, prefix });
397
399
  let response = await waitForDankMemer(10000);
398
400
  if (!response) return { totalRounds: 0, totalCaught: 0, totalMines: 0, results: [] };
399
401
 
@@ -437,7 +439,7 @@ async function runFishLoop({ channel, waitForDankMemer, maxRounds = 50, onRound,
437
439
  await sellAllFish({ channel, waitForDankMemer });
438
440
 
439
441
  // Re-send command to get fresh dashboard after sell
440
- await channel.send('pls fish catch');
442
+ await sendCommand(channel, 'fish catch', { useSlash, prefix });
441
443
  let newDash = await waitForDankMemer(8000);
442
444
  if (newDash) {
443
445
  let freshDash = await refetchMsg(channel, newDash.id);
@@ -569,7 +571,7 @@ async function runFishLoop({ channel, waitForDankMemer, maxRounds = 50, onRound,
569
571
  // Approach 2: If Fish Again not found, re-send "pls fish catch" fresh
570
572
  if (!fishAgainClicked) {
571
573
  LOG.debug('[fish] Fish Again not found, re-sending pls fish catch');
572
- await channel.send('pls fish catch');
574
+ await sendCommand(channel, 'fish catch', { useSlash, prefix });
573
575
  let newResp = await waitForDankMemer(8000);
574
576
  if (!newResp) { LOG.warn('[fish] No response to re-send'); break; }
575
577
  let freshResp = await refetchMsg(channel, newResp.id);
@@ -12,6 +12,7 @@
12
12
  const {
13
13
  LOG, c, getFullText, parseCoins, parseNetCoins, getAllButtons, safeClickButton,
14
14
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, ensureCV2, checkLevelLock,
15
+ sendCommand,
15
16
  } = require('./utils');
16
17
  const { EMA, AhoCorasick, SlidingWindowCounter } = require('../structures');
17
18
 
@@ -143,10 +144,11 @@ async function clickFast(msg, btn, retries = 2) {
143
144
  /**
144
145
  * Cointoss — CV2 format. Pick Heads/Tails randomly.
145
146
  */
146
- async function runCointoss({ channel, waitForDankMemer, betAmount = 10000 }) {
147
- const cmd = `pls cointoss ${betAmount}`;
147
+ async function runCointoss({ channel, waitForDankMemer, betAmount = 10000, useSlash, prefix = 'pls' }) {
148
+ const cmdName = 'cointoss';
149
+ const cmd = `${prefix} ${cmdName} ${betAmount}`;
148
150
  LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
149
- await channel.send(cmd);
151
+ await sendCommand(channel, cmdName, { useSlash, prefix, args: [betAmount] });
150
152
  let response = await waitForDankMemer(12000);
151
153
 
152
154
  await ensureCV2(response);
@@ -184,12 +186,13 @@ async function runCointoss({ channel, waitForDankMemer, betAmount = 10000 }) {
184
186
  * Roulette — pick color, click button, wait for result.
185
187
  * Strategy: ~47.5% red, ~47.5% black, ~5% green.
186
188
  */
187
- async function runRoulette({ channel, waitForDankMemer, betAmount = 10000 }) {
189
+ async function runRoulette({ channel, waitForDankMemer, betAmount = 10000, useSlash, prefix = 'pls' }) {
188
190
  const roll = Math.random();
189
191
  const color = roll < 0.475 ? 'red' : roll < 0.95 ? 'black' : 'green';
190
- const cmd = `pls roulette ${betAmount} ${color}`;
192
+ const cmdName = 'roulette';
193
+ const cmd = `${prefix} ${cmdName} ${betAmount} ${color}`;
191
194
  LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
192
- await channel.send(cmd);
195
+ await sendCommand(channel, cmdName, { useSlash, prefix, args: [betAmount, color] });
193
196
  let response = await waitForDankMemer(12000);
194
197
 
195
198
  const chk = await commonChecks(response, 'roulette');
@@ -227,10 +230,11 @@ async function runRoulette({ channel, waitForDankMemer, betAmount = 10000 }) {
227
230
  /**
228
231
  * Slots — auto-spin, wait for animation to finish, parse result.
229
232
  */
230
- async function runSlots({ channel, waitForDankMemer, betAmount = 10000 }) {
231
- const cmd = `pls slots ${betAmount}`;
233
+ async function runSlots({ channel, waitForDankMemer, betAmount = 10000, useSlash, prefix = 'pls' }) {
234
+ const cmdName = 'slots';
235
+ const cmd = `${prefix} ${cmdName} ${betAmount}`;
232
236
  LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
233
- await channel.send(cmd);
237
+ await sendCommand(channel, cmdName, { useSlash, prefix, args: [betAmount] });
234
238
  let response = await waitForDankMemer(12000);
235
239
 
236
240
  const chk = await commonChecks(response, 'slots');
@@ -258,10 +262,11 @@ async function runSlots({ channel, waitForDankMemer, betAmount = 10000 }) {
258
262
  /**
259
263
  * Snakeeyes — auto-roll, wait for animation to finish, parse result.
260
264
  */
261
- async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 10000 }) {
262
- const cmd = `pls snakeeyes ${betAmount}`;
265
+ async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 10000, useSlash, prefix = 'pls' }) {
266
+ const cmdName = 'snakeeyes';
267
+ const cmd = `${prefix} ${cmdName} ${betAmount}`;
263
268
  LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
264
- await channel.send(cmd);
269
+ await sendCommand(channel, cmdName, { useSlash, prefix, args: [betAmount] });
265
270
  let response = await waitForDankMemer(12000);
266
271
 
267
272
  const chk = await commonChecks(response, 'snakeeyes');
@@ -11,7 +11,7 @@
11
11
  const {
12
12
  LOG, c, getFullText, parseCoins, getAllButtons, getAllSelectMenus,
13
13
  safeClickButton, logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, needsItem,
14
- isCV2, ensureCV2, stripAnsi,
14
+ isCV2, ensureCV2, stripAnsi, sendCommand,
15
15
  } = require('./utils');
16
16
  const { buyItem } = require('./shop');
17
17
  const { AhoCorasick, LRUCache } = require('../structures');
@@ -50,10 +50,15 @@ async function waitForEditedMessage(channel, messageId, baselineText, timeoutMs
50
50
  * @param {object} [opts.client]
51
51
  * @returns {Promise<{result: string, coins: number}>}
52
52
  */
53
- async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, client }) {
53
+ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, client, useSlash, prefix = 'pls' }) {
54
54
  LOG.cmd(`${c.white}${c.bold}${cmdString}${c.reset}`);
55
-
56
- await channel.send(cmdString);
55
+ // cmdString = "pls farm view" or "/farm view"
56
+ // Build slash command: strip prefix → "farm view" → sendSlash('farm view')
57
+ const slashName = cmdString
58
+ .replace(/^pls\s+/, '')
59
+ .replace(/^\/\s*/, '')
60
+ .trim();
61
+ await sendCommand(channel, slashName, { useSlash, prefix });
57
62
  let response = await waitForDankMemer(10000);
58
63
 
59
64
  if (!response) {
@@ -82,7 +87,7 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
82
87
  if (bought) {
83
88
  LOG.success(`[${cmdName}] Bought ${missing}, retrying command...`);
84
89
  await sleep(1500);
85
- await channel.send(cmdString);
90
+ await sendCommand(channel, cmdName, { useSlash, prefix });
86
91
  const r2 = await waitForDankMemer(10000);
87
92
  if (r2) {
88
93
  logMsg(r2, `${cmdName}-retry`);
@@ -209,7 +214,7 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
209
214
  async function runAlert({ channel, waitForDankMemer }) {
210
215
  LOG.cmd(`${c.white}${c.bold}pls alert${c.reset}`);
211
216
 
212
- await channel.send('pls alert');
217
+ await sendCommand(channel, 'alert', { useSlash, prefix });
213
218
  const response = await waitForDankMemer(10000);
214
219
 
215
220
  if (!response) return { result: 'no response', coins: 0 };
@@ -7,6 +7,7 @@
7
7
  const {
8
8
  LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
9
9
  logMsg, isHoldTight, getHoldTightReason, sleep, checkLevelLock,
10
+ sendCommand,
10
11
  } = require('./utils');
11
12
 
12
13
  const RE_HINT_BOLD = /hint.*?\*\*(\d+)\*\*/i;
@@ -129,7 +130,7 @@ async function playHighLow(response, depth = 0) {
129
130
  async function runHighLow({ channel, waitForDankMemer }) {
130
131
  LOG.cmd(`${c.white}${c.bold}pls hl${c.reset}`);
131
132
 
132
- await channel.send('pls hl');
133
+ await sendCommand(channel, 'hl', { useSlash, prefix });
133
134
  const response = await waitForDankMemer(12000);
134
135
 
135
136
  if (!response) {
@@ -8,6 +8,7 @@ const {
8
8
  LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
9
9
  logMsg, isHoldTight, getHoldTightReason, sleep, needsItem,
10
10
  isCV2, ensureCV2, stripAnsi, checkLevelLock,
11
+ sendCommand,
11
12
  } = require('./utils');
12
13
  const { buyItem } = require('./shop');
13
14
 
@@ -80,7 +81,7 @@ function pickDodgeLane(dragonPos, buttons) {
80
81
  async function runHunt({ channel, waitForDankMemer, client }) {
81
82
  LOG.cmd(`${c.white}${c.bold}pls hunt${c.reset}`);
82
83
 
83
- await channel.send('pls hunt');
84
+ await sendCommand(channel, 'hunt', { useSlash, prefix });
84
85
  const response = await waitForDankMemer(12000);
85
86
 
86
87
  if (!response) {
@@ -118,7 +119,7 @@ async function runHunt({ channel, waitForDankMemer, client }) {
118
119
  LOG.success('[hunt] Rifle bought! Retrying...');
119
120
  while (await waitForDankMemer(1500)) {}
120
121
  await sleep(1000);
121
- await channel.send('pls hunt');
122
+ await sendCommand(channel, 'hunt', { useSlash, prefix });
122
123
  const r2 = await waitForDankMemer(12000);
123
124
  if (r2) {
124
125
  if (isCV2(r2)) await ensureCV2(r2);
@@ -15,6 +15,7 @@ const {
15
15
  LOG, c, sleep, humanDelay, getFullText, getAllButtons,
16
16
  safeClickButton, logMsg, isHoldTight, getHoldTightReason,
17
17
  isCV2, ensureCV2,
18
+ sendCommand,
18
19
  } = require('./utils');
19
20
  const { Trie, LRUCache } = require('../structures');
20
21
 
@@ -197,7 +198,7 @@ async function enrichItems(items) {
197
198
  async function runInventory({ channel, waitForDankMemer, client, accountId, redis, onPageProgress }) {
198
199
  LOG.cmd(`${c.white}${c.bold}pls inv${c.reset}`);
199
200
 
200
- await channel.send('pls inv');
201
+ await sendCommand(channel, 'inv', { useSlash, prefix });
201
202
  let response = await waitForDankMemer(10000);
202
203
 
203
204
  if (!response) {
@@ -16,6 +16,7 @@ const {
16
16
  LOG, c, getFullText, parseCoins, getAllButtons, getAllSelectMenus,
17
17
  findButton, safeClickButton, logMsg, isHoldTight, getHoldTightReason,
18
18
  sleep, humanDelay, checkLevelLock,
19
+ sendCommand,
19
20
  } = require('./utils');
20
21
 
21
22
  const RE_COOLDOWN_MIN = /(\d+)\s*minute/i;
@@ -37,7 +38,7 @@ async function refetchMsg(channel, msgId) {
37
38
  async function runPostMemes({ channel, waitForDankMemer }) {
38
39
  LOG.cmd(`${c.white}${c.bold}pls pm${c.reset}`);
39
40
 
40
- await channel.send('pls pm');
41
+ await sendCommand(channel, 'pm', { useSlash, prefix });
41
42
  let response = await waitForDankMemer(10000);
42
43
 
43
44
  if (!response) {
@@ -7,6 +7,7 @@
7
7
 
8
8
  const {
9
9
  LOG, c, getFullText, logMsg, isHoldTight, sleep, stripAnsi,
10
+ sendCommand,
10
11
  } = require('./utils');
11
12
 
12
13
  const CACHE_TTL = 10 * 60 * 1000; // 10 minutes
@@ -175,7 +176,7 @@ async function getPlayerLevel({ channel, waitForDankMemer, accountId = 'default'
175
176
  }
176
177
  } catch {}
177
178
  }
178
- await channel.send('pls profile');
179
+ await sendCommand(channel, 'profile', { useSlash, prefix });
179
180
  const response = await waitForDankMemer(8000);
180
181
  if (!response) { LOG.warn('[profile] No response'); return null; }
181
182
  if (isHoldTight(response)) { await sleep(5000); return null; }
@@ -202,7 +203,7 @@ async function getPlayerLevel({ channel, waitForDankMemer, accountId = 'default'
202
203
  */
203
204
  async function runProfile({ channel, waitForDankMemer, accountId = 'default', redis }) {
204
205
  LOG.cmd(`${c.white}${c.bold}pls profile${c.reset}`);
205
- await channel.send('pls profile');
206
+ await sendCommand(channel, 'profile', { useSlash, prefix });
206
207
  const response = await waitForDankMemer(8000);
207
208
  if (!response) {
208
209
  LOG.warn('[profile] No response');
@@ -7,6 +7,7 @@
7
7
  const {
8
8
  LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
9
9
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, checkLevelLock,
10
+ sendCommand,
10
11
  } = require('./utils');
11
12
  const { meetsLevelRequirement } = require('./profile');
12
13
 
@@ -27,7 +28,7 @@ async function runScratch({ channel, waitForDankMemer, accountId, redis }) {
27
28
  }
28
29
  LOG.cmd(`${c.white}${c.bold}pls scratch${c.reset}`);
29
30
 
30
- await channel.send('pls scratch');
31
+ await sendCommand(channel, 'scratch', { useSlash, prefix });
31
32
  const response = await waitForDankMemer(10000);
32
33
 
33
34
  if (!response) {
@@ -12,6 +12,7 @@ const {
12
12
  LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
13
13
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
14
14
  isCV2, ensureCV2, checkLevelLock,
15
+ sendCommand,
15
16
  } = require('./utils');
16
17
  const { VoseAlias, Trie, EMA, LRUCache } = require('../structures');
17
18
 
@@ -84,7 +85,7 @@ function pickSafeButton(buttons, customSafe) {
84
85
  async function runSearch({ channel, waitForDankMemer, safeAnswers }) {
85
86
  LOG.cmd(`${c.white}${c.bold}pls search${c.reset}`);
86
87
 
87
- await channel.send('pls search');
88
+ await sendCommand(channel, 'search', { useSlash, prefix });
88
89
  const response = await waitForDankMemer(10000);
89
90
 
90
91
  if (!response) {
@@ -17,6 +17,7 @@ const {
17
17
  LOG, c, sleep, humanDelay, getFullText,
18
18
  getAllButtons, findSelectMenuOption, getAllSelectMenus, safeClickButton,
19
19
  logMsg, isHoldTight, isCV2, ensureCV2, stripAnsi,
20
+ sendCommand,
20
21
  } = require('./utils');
21
22
  const { LRUCache, Trie } = require('../structures');
22
23
 
@@ -135,7 +136,7 @@ function normalizeShopRequest(itemName, quantity = 1) {
135
136
  }
136
137
 
137
138
  async function openShopView({ channel, waitForDankMemer }) {
138
- await channel.send('pls shop view');
139
+ await sendCommand(channel, 'shop view', { useSlash, prefix });
139
140
  let response = await waitForDankMemer(10000);
140
141
 
141
142
  if (!response) return { ok: false, reason: 'no-response', response: null };
@@ -2,6 +2,7 @@ const {
2
2
  LOG, c, getFullText, parseCoins, getAllButtons, getAllSelectMenus,
3
3
  safeClickButton, logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, needsItem,
4
4
  isCV2, ensureCV2, stripAnsi, clickCV2Button, checkLevelLock,
5
+ sendCommand,
5
6
  } = require('./utils');
6
7
  const { buyItem } = require('./shop');
7
8
 
@@ -196,7 +197,7 @@ async function selectRandomStreamOption(msg) {
196
197
  async function runStream({ channel, waitForDankMemer, client }) {
197
198
  LOG.cmd(`${c.white}${c.bold}pls stream${c.reset}`);
198
199
 
199
- await channel.send('pls stream');
200
+ await sendCommand(channel, 'stream', { useSlash, prefix });
200
201
  let response = await waitForDankMemer(12000);
201
202
 
202
203
  if (!response) {
@@ -238,7 +239,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
238
239
  }
239
240
 
240
241
  await sleep(800);
241
- await channel.send('pls stream');
242
+ await sendCommand(channel, 'stream', { useSlash, prefix });
242
243
  response = await waitForDankMemer(12000);
243
244
  if (!response) return { result: 'no response after buy', coins: 0, nextCooldownSec: 180 };
244
245
  await hydrate(response);
@@ -10,6 +10,7 @@
10
10
  const {
11
11
  LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
12
12
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, checkLevelLock,
13
+ sendCommand,
13
14
  } = require('./utils');
14
15
 
15
16
  const RE_TRIVIA_MD_BOLD = /\*\*/g;
@@ -79,7 +80,7 @@ async function learnFromResult(question, correctAnswer, redis) {
79
80
  async function runTrivia({ channel, waitForDankMemer, redis }) {
80
81
  LOG.cmd(`${c.white}${c.bold}pls trivia${c.reset}`);
81
82
 
82
- await channel.send('pls trivia');
83
+ await sendCommand(channel, 'trivia', { useSlash, prefix });
83
84
  const response = await waitForDankMemer(10000);
84
85
 
85
86
  if (!response) {
@@ -54,6 +54,34 @@ const c = {
54
54
  magenta: '\x1b[35m', white: '\x1b[37m', blue: '\x1b[34m',
55
55
  };
56
56
 
57
+ // ── Slash Command Helper (dmg pattern) ──────────────────────
58
+ // Wraps channel.send for slash commands — uses Discord interaction API
59
+ // when use_slash=true, falls back to text prefix (pls or /) otherwise.
60
+ // Usage: await sendCommand(channel, 'adventure', { useSlash: true, prefix: '/' })
61
+ async function sendCommand(channel, commandName, opts = {}) {
62
+ const {
63
+ useSlash = false,
64
+ prefix = 'pls',
65
+ args = [],
66
+ } = opts;
67
+
68
+ if (useSlash && channel?.sendSlash) {
69
+ try {
70
+ // sendSlash takes botId, commandName, ...args
71
+ await channel.sendSlash(DANK_MEMER_ID, commandName, ...args);
72
+ return;
73
+ } catch (e) {
74
+ LOG.warn(`[sendSlash] ${commandName} failed: ${e.message} — falling back to text`);
75
+ }
76
+ }
77
+
78
+ // Fallback: text command
79
+ const textCmd = args.length > 0
80
+ ? `${prefix} ${commandName} ${args.join(' ')}`
81
+ : `${prefix} ${commandName}`;
82
+ await channel.send(textCmd);
83
+ }
84
+
57
85
  // ── Logging ──────────────────────────────────────────────────
58
86
  // When dashboard is active, suppress direct console output from command handlers.
59
87
  // grinder.js sets this to true once the live dashboard starts rendering.
@@ -765,6 +793,7 @@ module.exports = {
765
793
  getFullText,
766
794
  stripAnsi,
767
795
  parseCoins,
796
+ sendCommand,
768
797
  parseNetCoins,
769
798
  parseBalance,
770
799
  getAllButtons,
@@ -19,6 +19,7 @@ const {
19
19
  LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
20
20
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
21
21
  isCV2, ensureCV2, stripAnsi, checkLevelLock,
22
+ sendCommand,
22
23
  } = require('./utils');
23
24
 
24
25
  const RE_MEMORY_BACKTICK_CHUNK = /`([^`]+)`/g;
@@ -142,7 +143,7 @@ function parseWorkCooldown(text) {
142
143
  */
143
144
  async function resignFromJob({ channel, waitForDankMemer }) {
144
145
  LOG.info('[work] Resigning from current job...');
145
- await channel.send('pls work resign');
146
+ await sendCommand(channel, 'work resign', { useSlash, prefix });
146
147
  const response = await waitForDankMemer(8000);
147
148
  if (!response) { LOG.warn('[work] No response to resign'); return false; }
148
149
  logMsg(response, 'work-resign');
@@ -175,7 +176,7 @@ async function resignFromJob({ channel, waitForDankMemer }) {
175
176
  */
176
177
  async function autoApplyForJob({ channel, waitForDankMemer }) {
177
178
  LOG.info('[work] No job! Applying for Babysitter...');
178
- await channel.send('pls work apply babysitter');
179
+ await sendCommand(channel, 'work apply babysitter', { useSlash, prefix });
179
180
  const response = await waitForDankMemer(10000);
180
181
  if (!response) {
181
182
  LOG.warn('[work] No response to apply');
@@ -406,7 +407,7 @@ async function handleGenericMinigame({ channel, current, waitForDankMemer }) {
406
407
  async function runWorkShift({ channel, waitForDankMemer }) {
407
408
  LOG.cmd(`${c.white}${c.bold}pls work shift${c.reset}`);
408
409
 
409
- await channel.send('pls work shift');
410
+ await sendCommand(channel, 'work shift', { useSlash, prefix });
410
411
  let current = await waitForDankMemer(10000);
411
412
 
412
413
  if (!current) {
@@ -453,7 +454,7 @@ async function runWorkShift({ channel, waitForDankMemer }) {
453
454
  while (await waitForDankMemer(1500)) {}
454
455
  await sleep(500);
455
456
 
456
- await channel.send('pls work shift');
457
+ await sendCommand(channel, 'work shift', { useSlash, prefix });
457
458
  current = await waitForDankMemer(10000);
458
459
  if (!current) return { result: 'no response after apply', coins: 0, nextCooldownSec: 180 };
459
460
  if (isCV2(current)) await ensureCV2(current);
package/lib/grinder.js CHANGED
@@ -598,6 +598,37 @@ class MinHeap {
598
598
 
599
599
  get size() { return this.heap.length; }
600
600
 
601
+ // ── Message handler registration (dmg pattern) ──────────────
602
+ // Allows plugins/commands to register handlers for gateway events.
603
+ // handlers: { messageCreate: Map<name, fn>, messageUpdate: Map<name, fn> }
604
+ // ── Dispatch events to registered handlers (dmg pattern) ──
605
+ _dispatchToHandlers(event, msg) {
606
+ const handlers = this._handlers[event];
607
+ if (!handlers || handlers.size === 0) return;
608
+ for (const [name, fn] of handlers) {
609
+ try {
610
+ const result = fn(msg);
611
+ if (result && typeof result.then === 'function') {
612
+ result.catch(e => this.log('error', `[handler:${name}] ${e.message}`));
613
+ }
614
+ } catch (e) {
615
+ this.log('error', `[handler:${name}] ${e.message}`);
616
+ }
617
+ }
618
+ }
619
+
620
+ _registerHandler(event, name, fn) {
621
+ if (this._handlers[event]) {
622
+ this._handlers[event].set(name, fn);
623
+ }
624
+ }
625
+
626
+ _unregisterHandler(event, name) {
627
+ if (this._handlers[event]) {
628
+ this._handlers[event].delete(name);
629
+ }
630
+ }
631
+
601
632
  _bubbleUp(i) {
602
633
  while (i > 0) {
603
634
  const parent = (i - 1) >> 1;
@@ -677,6 +708,14 @@ class AccountWorker {
677
708
  this._commandRunning = false; // prevents grinding commands from overlapping with quest commands
678
709
  this._verifyLevelUnlock = null; // holds level to verify after quest completion
679
710
  this.commandQueue = null;
711
+
712
+ // ── Message handler routing (dmg pattern) ─────────────────
713
+ // Map of event → handler functions. Plugins register here.
714
+ // Usage: _handlers.messageCreate.get('adventure')?.(msg)
715
+ this._handlers = {
716
+ messageCreate: new Map(),
717
+ messageUpdate: new Map(),
718
+ };
680
719
  this.lastHealthCheck = Date.now();
681
720
  this.doneToday = new Map();
682
721
 
@@ -762,6 +801,8 @@ class AccountWorker {
762
801
  }
763
802
  function handler(msg) {
764
803
  if (msg.author.id === DANK_MEMER_ID && msg.channel.id === self.channel.id) {
804
+ // ── Dispatch to registered handlers (dmg pattern) ────
805
+ self._dispatchToHandlers('messageCreate', msg);
765
806
  const hasComponentPayload = Array.isArray(msg.components)
766
807
  && msg.components.some(c => c && (c.components || c.content || c.type || c.label || c.customId));
767
808
  const hasContent = (msg.content && msg.content.length > 0)
@@ -791,6 +832,8 @@ class AccountWorker {
791
832
  // Reject edits to messages created well before our command was sent
792
833
  const msgTs = newMsg.createdTimestamp || newMsg.createdAt?.getTime?.() || 0;
793
834
  if (msgTs > 0 && msgTs < sentAt - 1500) return;
835
+ // ── Dispatch to registered handlers ─────────────────
836
+ self._dispatchToHandlers('messageUpdate', newMsg);
794
837
  cleanup();
795
838
  resolve(newMsg);
796
839
  }
@@ -1356,6 +1399,9 @@ class AccountWorker {
1356
1399
  client: this.client,
1357
1400
  safeAnswers: cmdName === 'search' ? safeParseJSON(this.account.search_answers, []) :
1358
1401
  cmdName === 'crime' ? safeParseJSON(this.account.crime_answers, []) : [],
1402
+ adventureAnswers: AccountWorker.ADVENTURE_ANSWERS,
1403
+ useSlash: !!this.account.use_slash,
1404
+ prefix: this.account.use_slash ? '/' : 'pls',
1359
1405
  betAmount: this._questBetOverride || (['blackjack'].includes(cmdName) ? bjBet : gambBet),
1360
1406
  accountId: this.account.id,
1361
1407
  redis,
@@ -1821,6 +1867,42 @@ class AccountWorker {
1821
1867
  ].map(Object.freeze);
1822
1868
 
1823
1869
  // ── Level Quest System — blocks grinding until quests complete ──
1870
+ // ── Adventure answer maps (per type) ──────────────────────────
1871
+ // Pattern: question text → answer choice to click
1872
+ // Supports: space, out west, brazil, vacation, halloween, museum
1873
+ static ADVENTURE_ANSWERS = {
1874
+ space: {
1875
+ 'what do you want to do': ['search for debris', 'scavenge wreckage'],
1876
+ 'how do you respond': ['stay calm', 'analyze situation'],
1877
+ 'what do you choose': ['try to communicate', 'observe first'],
1878
+ },
1879
+ west: {
1880
+ 'what do you do': ['draw first', 'hide behind cover'],
1881
+ 'how do you respond': ['draw', 'shoot'],
1882
+ 'sheriff wants': ['accept bounty', 'decline'],
1883
+ },
1884
+ brazil: {
1885
+ 'what do you want to do': ['search for treasure', 'ask locals'],
1886
+ 'how do you respond': ['negotiate', 'accept offer'],
1887
+ 'locals offer': ['take the deal', 'refuse'],
1888
+ },
1889
+ vacation: {
1890
+ 'stranger offers': ['accept', 'decline'],
1891
+ 'what do you do': ['explore', 'rest'],
1892
+ 'something suspicious': ['investigate', 'ignore'],
1893
+ },
1894
+ halloween: {
1895
+ 'knock at door': ['open it', 'hide'],
1896
+ 'what do you do': ['investigate', 'run away'],
1897
+ 'ghost appears': ['speak to it', 'hide'],
1898
+ },
1899
+ museum: {
1900
+ 'what do you do': ['touch exhibit', 'keep distance'],
1901
+ 'alarm sounds': ['hide', 'run'],
1902
+ 'guard asks': ['tell truth', 'lie'],
1903
+ },
1904
+ };
1905
+
1824
1906
  static LEVEL_QUESTS = {
1825
1907
  1: [
1826
1908
  { cmd: 'beg', times: 2 },
package/lib/rawLogger.js CHANGED
@@ -457,6 +457,20 @@ async function store(d, event) {
457
457
  pipe.ltrim(accKey, 0, 4999);
458
458
  pipe.expire(accKey, LOG_TTL);
459
459
  }
460
+ // Track ephemeral in memory for quick access before they vanish
461
+ if (parsed.isEphemeral || (d.flags & 32832)) {
462
+ let chMap = ephemeralByChannel.get(d.channel_id);
463
+ if (!chMap) {
464
+ chMap = new Map();
465
+ ephemeralByChannel.set(d.channel_id, chMap);
466
+ }
467
+ chMap.set(d.id, parsed);
468
+ // Cap per-channel ephemeral buffer at 50
469
+ if (chMap.size > 50) {
470
+ const firstKey = chMap.keys().next().value;
471
+ chMap.delete(firstKey);
472
+ }
473
+ }
460
474
  // Ephemeral log
461
475
  if (parsed.isEphemeral || (d.flags & 32832)) {
462
476
  pipe.lpush('raw:ephemeral:log', `${d.id}:${parsed._capturedAt}:${parsed.command}:${d.channel_id}`);
@@ -494,8 +508,17 @@ function onDmEvent(fn) { dmListeners.push(fn); }
494
508
 
495
509
  // ── Ephemeral message callbacks ──
496
510
  const ephemeralListeners = [];
511
+ const ephemeralByChannel = new Map(); // channelId → Map<msgId, parsed msg>
512
+
497
513
  function onNextEphemeral(fn) { ephemeralListeners.push(fn); }
498
514
 
515
+ // Track ephemeral messages per channel — useful for reading state before vanish
516
+ function getEphemeralMsgs(channelId, count = 20) {
517
+ const map = ephemeralByChannel.get(channelId);
518
+ if (!map) return [];
519
+ return [...map.values()].slice(-count);
520
+ }
521
+
499
522
  function _notifyEphemeral(parsed) {
500
523
  if (!ephemeralListeners.length) return;
501
524
  const listeners = [...ephemeralListeners];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.109.0",
3
+ "version": "8.111.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"