dankgrinder 4.6.0 → 4.8.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.
@@ -34,11 +34,10 @@
34
34
  */
35
35
 
36
36
  const {
37
- LOG, c, sleep, humanDelay, getFullText, parseCoins, parseBalance,
38
- getAllButtons, getAllSelectMenus, findButton, findSelectMenuOption,
39
- safeClickButton, isHoldTight, logMsg, dumpMessage, needsItem,
37
+ LOG, c, sleep, humanDelay, getFullText, parseCoins,
38
+ getAllButtons, getAllSelectMenus, findButton,
39
+ safeClickButton, isHoldTight, logMsg,
40
40
  } = require('./utils');
41
- const { buyItem } = require('./shop');
42
41
 
43
42
  // ── Adventure type rotation (cycle through all types each run) ────
44
43
  let lastAdventureIndex = -1;
@@ -63,8 +62,7 @@ async function clickAndRefetch(channel, msg, btn) {
63
62
  LOG.error(`[adventure] Click error: ${e.message}`);
64
63
  return null;
65
64
  }
66
- // Dank Memer edits the message after a button click
67
- await sleep(500);
65
+ await sleep(250);
68
66
  return await refetchMsg(channel, msg.id);
69
67
  }
70
68
 
@@ -172,7 +170,7 @@ async function playAdventureRounds(channel, msg) {
172
170
  const choice = pickSafeChoice(choices);
173
171
  if (choice) {
174
172
  LOG.info(`[adventure] → Choosing: "${choice.label}"`);
175
- await sleep(200);
173
+ await sleep(100);
176
174
  const afterChoice = await clickAndRefetch(channel, current, choice);
177
175
  if (afterChoice) {
178
176
  current = afterChoice;
@@ -194,7 +192,7 @@ async function playAdventureRounds(channel, msg) {
194
192
  const nextBtnNow = getNextButton(current);
195
193
  if (nextBtnNow && !nextBtnNow.disabled) {
196
194
  LOG.debug(`[adventure] Clicking Next arrow...`);
197
- await sleep(200);
195
+ await sleep(100);
198
196
  const afterNext = await clickAndRefetch(channel, current, nextBtnNow);
199
197
  if (afterNext) {
200
198
  current = afterNext;
@@ -206,7 +204,7 @@ async function playAdventureRounds(channel, msg) {
206
204
  } else if (nextBtnNow && nextBtnNow.disabled) {
207
205
  // Next is disabled but no choices found — might be loading
208
206
  LOG.debug(`[adventure] Next disabled, no choices — waiting...`);
209
- await sleep(600);
207
+ await sleep(300);
210
208
  const refreshed = await refetchMsg(channel, current.id);
211
209
  if (refreshed) {
212
210
  current = refreshed;
@@ -219,8 +217,7 @@ async function playAdventureRounds(channel, msg) {
219
217
  // No next button at all
220
218
  LOG.debug(`[adventure] No Next button — checking if done`);
221
219
  if (isAdventureDone(current)) break;
222
- // Wait and re-fetch as Dank Memer might still be editing
223
- await sleep(1500);
220
+ await sleep(500);
224
221
  const refreshed = await refetchMsg(channel, current.id);
225
222
  if (refreshed) {
226
223
  current = refreshed;
@@ -314,42 +311,10 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
314
311
  return { result: 'cooldown', coins: 0, nextCooldownSec: cooldownSec + 3 };
315
312
  }
316
313
 
317
- // ── 4) If we need a ticket, try to buy one ─────────────────
314
+ // ── 4) If we need a ticket, skip (too expensive to auto-buy) ──
318
315
  if (needsTicket) {
319
- LOG.warn(`[adventure] Need adventure ticket! Attempting to buy...`);
320
-
321
- // Ticket costs 250,000 coins — check balance first to avoid wasting time in shop
322
- const TICKET_COST = 250000;
323
- let currentBalance = 0;
324
- await channel.send('pls bal');
325
- const balMsg = await waitForDankMemer(8000);
326
- if (balMsg) {
327
- currentBalance = parseBalance(balMsg);
328
- LOG.info(`[adventure] Balance: ${c.yellow}⏣ ${currentBalance.toLocaleString()}${c.reset} (ticket costs ⏣ ${TICKET_COST.toLocaleString()})`);
329
- }
330
-
331
- if (currentBalance < TICKET_COST) {
332
- LOG.warn(`[adventure] Not enough coins for ticket (⏣ ${currentBalance.toLocaleString()} < ⏣ ${TICKET_COST.toLocaleString()}). Grind more first.`);
333
- return { result: `need ticket (⏣ ${currentBalance.toLocaleString()}/${TICKET_COST.toLocaleString()})`, coins: 0, nextCooldownSec: 120 };
334
- }
335
-
336
- const bought = await buyItem({
337
- channel, waitForDankMemer, client,
338
- itemName: 'Adventure Ticket', quantity: 1,
339
- });
340
-
341
- if (!bought) {
342
- LOG.error('[adventure] Could not buy adventure ticket from shop.');
343
- return { result: 'need ticket (buy failed)', coins: 0, nextCooldownSec: 120 };
344
- }
345
-
346
- LOG.success('[adventure] Tickets purchased! Re-running adventure...');
347
- await sleep(1500);
348
-
349
- await channel.send('pls adventure');
350
- response = await waitForDankMemer(12000);
351
- if (!response) return { result: 'no response after ticket buy', coins: 0, nextCooldownSec: null };
352
- logMsg(response, 'adventure-after-buy');
316
+ LOG.warn(`[adventure] No ticket skipping (too expensive to auto-buy)`);
317
+ return { result: 'no ticket', coins: 0, nextCooldownSec: 3600, skipReason: 'no_ticket' };
353
318
  }
354
319
 
355
320
  // ── Check if we're already mid-adventure (no select menu) ──
@@ -45,7 +45,7 @@ async function runDig({ channel, waitForDankMemer, client }) {
45
45
  const bought = await buyItem({
46
46
  channel, waitForDankMemer, client,
47
47
  itemName: 'Shovel',
48
- quantity: 3,
48
+ quantity: 1,
49
49
  });
50
50
 
51
51
  if (bought) {
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Simple gambling command handlers.
3
- * Covers: coinflip, roulette, slots, snakeeyes
2
+ * Gambling command handlers.
3
+ * Covers: cointoss, roulette, slots, snakeeyes
4
4
  */
5
5
 
6
6
  const {
@@ -8,6 +8,29 @@ const {
8
8
  logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
9
9
  } = require('./utils');
10
10
 
11
+ function parseGambleResult(text, cmdName) {
12
+ const net = parseNetCoins(text);
13
+ const lower = text.toLowerCase();
14
+
15
+ if (net > 0) {
16
+ LOG.coin(`[${cmdName}] ${c.green}+⏣ ${net.toLocaleString()}${c.reset}`);
17
+ return { result: `${cmdName} → +⏣ ${net.toLocaleString()}`, coins: net };
18
+ }
19
+ if (net < 0) {
20
+ LOG.warn(`[${cmdName}] ${c.red}-⏣ ${Math.abs(net).toLocaleString()}${c.reset}`);
21
+ return { result: `${cmdName} → -⏣ ${Math.abs(net).toLocaleString()}`, coins: 0, lost: Math.abs(net) };
22
+ }
23
+ const coins = parseCoins(text);
24
+ if (coins > 0) {
25
+ LOG.coin(`[${cmdName}] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
26
+ return { result: `${cmdName} → +⏣ ${coins.toLocaleString()}`, coins };
27
+ }
28
+ if (lower.includes('won') || lower.includes('beat')) return { result: `${cmdName} → won`, coins: 0 };
29
+ if (lower.includes('lost') || lower.includes('bust')) return { result: `${cmdName} → lost`, coins: 0, lost: coins };
30
+
31
+ return { result: `${cmdName} done`, coins: 0 };
32
+ }
33
+
11
34
  async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
12
35
  LOG.cmd(`${c.white}${c.bold}${cmdString}${c.reset}`);
13
36
 
@@ -26,16 +49,29 @@ async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
26
49
  return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
27
50
  }
28
51
 
52
+ // Check for min bet error
53
+ const initText = getFullText(response);
54
+ const initLower = initText.toLowerCase();
55
+ if (initLower.includes("can't bet less than") || initLower.includes('cannot bet less than') || initLower.includes('minimum bet')) {
56
+ const betMatch = initText.match(/less than\s*\*?\*?[⏣o]?\s*([\d,]+)/i) || initText.match(/(\d[\d,]+)/);
57
+ if (betMatch) {
58
+ const minBet = parseInt(betMatch[1].replace(/,/g, ''));
59
+ if (minBet > 0) return { result: `min bet ${minBet}`, coins: 0, newMinBet: minBet };
60
+ }
61
+ return { result: 'min bet error', coins: 0 };
62
+ }
63
+
29
64
  logMsg(response, cmdName);
30
65
  const text = getFullText(response);
31
66
 
32
- // Some gambles have buttons (coinflip: pick heads/tails)
67
+ // For cointoss/gambles with buttons: click a random non-disabled button
33
68
  const buttons = getAllButtons(response);
34
69
  if (buttons.length > 0) {
35
- const btn = buttons.find(b => !b.disabled);
36
- if (btn) {
37
- LOG.info(`[${cmdName}] Clicking "${btn.label}"`);
38
- await humanDelay();
70
+ const clickable = buttons.filter(b => !b.disabled);
71
+ if (clickable.length > 0) {
72
+ const btn = clickable[Math.floor(Math.random() * clickable.length)];
73
+ LOG.info(`[${cmdName}] Clicking "${btn.label || '?'}"`);
74
+ await humanDelay(50, 200);
39
75
  try {
40
76
  const followUp = await safeClickButton(response, btn);
41
77
  if (followUp) {
@@ -50,32 +86,11 @@ async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
50
86
  return parseGambleResult(text, cmdName);
51
87
  }
52
88
 
53
- function parseGambleResult(text, cmdName) {
54
- const net = parseNetCoins(text);
55
- const lower = text.toLowerCase();
56
-
57
- if (net > 0) {
58
- LOG.coin(`[${cmdName}] ${c.green}+⏣ ${net.toLocaleString()}${c.reset}`);
59
- return { result: `${cmdName} → +⏣ ${net.toLocaleString()}`, coins: net };
60
- }
61
- if (net < 0) {
62
- LOG.warn(`[${cmdName}] ${c.red}-⏣ ${Math.abs(net).toLocaleString()}${c.reset}`);
63
- return { result: `${cmdName} → -⏣ ${Math.abs(net).toLocaleString()}`, coins: 0, lost: Math.abs(net) };
64
- }
65
- // Fallback to parseCoins
66
- const coins = parseCoins(text);
67
- if (coins > 0) {
68
- LOG.coin(`[${cmdName}] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
69
- return { result: `${cmdName} → +⏣ ${coins.toLocaleString()}`, coins };
70
- }
71
- if (lower.includes('won') || lower.includes('beat')) return { result: `${cmdName} → won`, coins: 0 };
72
- if (lower.includes('lost') || lower.includes('bust')) return { result: `${cmdName} → lost`, coins: 0, lost: coins };
73
-
74
- return { result: `${cmdName} done`, coins: 0 };
75
- }
76
-
77
- async function runCoinflip({ channel, waitForDankMemer, betAmount = 5000 }) {
78
- return runGamble({ channel, waitForDankMemer, cmdName: 'coinflip', cmdString: `pls coinflip ${betAmount} heads` });
89
+ /**
90
+ * Cointoss: send "pls cointoss <bet>", then click Heads or Tails randomly.
91
+ */
92
+ async function runCointoss({ channel, waitForDankMemer, betAmount = 10000 }) {
93
+ return runGamble({ channel, waitForDankMemer, cmdName: 'cointoss', cmdString: `pls cointoss ${betAmount}` });
79
94
  }
80
95
 
81
96
  async function runRoulette({ channel, waitForDankMemer, betAmount = 5000 }) {
@@ -90,4 +105,4 @@ async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 5000 }) {
90
105
  return runGamble({ channel, waitForDankMemer, cmdName: 'snakeeyes', cmdString: `pls snakeeyes ${betAmount}` });
91
106
  }
92
107
 
93
- module.exports = { runGamble, runCoinflip, runRoulette, runSlots, runSnakeeyes };
108
+ module.exports = { runGamble, runCointoss, runRoulette, runSlots, runSnakeeyes };
@@ -45,7 +45,7 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
45
45
  const missing = needsItem(text);
46
46
  if (missing) {
47
47
  LOG.warn(`[${cmdName}] Missing ${c.bold}${missing}${c.reset} — auto-buying...`);
48
- const bought = await buyItem({ channel, waitForDankMemer, client, itemName: missing, quantity: 3 });
48
+ const bought = await buyItem({ channel, waitForDankMemer, client, itemName: missing, quantity: 1 });
49
49
  if (bought) {
50
50
  LOG.success(`[${cmdName}] Bought ${missing}, retrying command...`);
51
51
  await sleep(3000);
@@ -65,11 +65,12 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
65
65
  return { result: `need ${missing} (buy failed)`, coins: 0 };
66
66
  }
67
67
 
68
- // Handle buttons if present
68
+ // Handle buttons if present — pick a random non-disabled button
69
69
  const buttons = getAllButtons(response);
70
70
  if (buttons.length > 0) {
71
- const btn = buttons.find(b => !b.disabled) || buttons[0];
72
- if (btn && !btn.disabled) {
71
+ const clickable = buttons.filter(b => !b.disabled);
72
+ const btn = clickable.length > 0 ? clickable[Math.floor(Math.random() * clickable.length)] : null;
73
+ if (btn) {
73
74
  LOG.info(`[${cmdName}] Clicking "${btn.label || '?'}"`);
74
75
  await humanDelay();
75
76
  try {
@@ -45,7 +45,7 @@ async function runHunt({ channel, waitForDankMemer, client }) {
45
45
  const bought = await buyItem({
46
46
  channel, waitForDankMemer, client,
47
47
  itemName: 'Hunting Rifle',
48
- quantity: 3,
48
+ quantity: 1,
49
49
  });
50
50
 
51
51
  if (bought) {
@@ -16,7 +16,7 @@ const { runScratch } = require('./scratch');
16
16
  const { runBlackjack } = require('./blackjack');
17
17
  const { runTrivia, triviaDB } = require('./trivia');
18
18
  const { runWorkShift } = require('./work');
19
- const { runCoinflip, runRoulette, runSlots, runSnakeeyes, runGamble } = require('./gamble');
19
+ const { runCointoss, runRoulette, runSlots, runSnakeeyes, runGamble } = require('./gamble');
20
20
  const { runDeposit } = require('./deposit');
21
21
  const { runGeneric, runAlert } = require('./generic');
22
22
  const { runStream } = require('./stream');
@@ -39,7 +39,7 @@ module.exports = {
39
39
  runBlackjack,
40
40
  runTrivia,
41
41
  runWorkShift,
42
- runCoinflip,
42
+ runCointoss,
43
43
  runRoulette,
44
44
  runSlots,
45
45
  runSnakeeyes,
@@ -32,8 +32,12 @@ const ITEM_COSTS = {
32
32
  * @returns {Promise<boolean>} true if purchase succeeded
33
33
  */
34
34
  async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, client }) {
35
- const MAX_RETRIES = 3;
36
- const searchName = itemName.toLowerCase().replace('hunting ', '').replace('fishing ', '').replace('adventure ', '');
35
+ const MAX_RETRIES = 1;
36
+ const searchNames = [
37
+ itemName.toLowerCase(),
38
+ itemName.toLowerCase().replace('hunting ', '').replace('fishing ', '').replace('adventure ', ''),
39
+ itemName.toLowerCase().split(' ').pop(),
40
+ ];
37
41
 
38
42
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
39
43
  LOG.buy(`Opening shop to buy ${c.bold}${quantity}x ${itemName}${c.reset} (attempt ${attempt}/${MAX_RETRIES})`);
@@ -120,30 +124,17 @@ async function buyItem({ channel, waitForDankMemer, itemName, quantity = 1, clie
120
124
  logMsg(response, 'shop-after-nav');
121
125
 
122
126
  // Step 3: Find the Buy button for our item
123
- let buyBtn = getAllButtons(response).find(b => b.label && b.label.toLowerCase().includes(searchName));
127
+ let buyBtn = getAllButtons(response).find(b => {
128
+ if (!b.label) return false;
129
+ const label = b.label.toLowerCase();
130
+ return searchNames.some(s => label.includes(s) || s.includes(label));
131
+ });
124
132
 
125
133
  if (!buyBtn) {
126
- LOG.warn(`No Buy button found for "${itemName}" (searched: "${searchName}")`);
127
- // Log all available buttons for debugging
134
+ LOG.warn(`No Buy button found for "${itemName}"`);
128
135
  const allBtns = getAllButtons(response);
129
136
  if (allBtns.length > 0) {
130
- LOG.debug(`Available buttons: ${allBtns.map(b => `"${b.label}"`).join(', ')}`);
131
- }
132
- // Maybe we need to scroll/paginate?
133
- const nextBtn = findButton(response, 'next') || findButton(response, '▶') || findButton(response, '→');
134
- if (nextBtn && !nextBtn.disabled) {
135
- LOG.buy('Clicking next page to find item...');
136
- try {
137
- const nextPage = await safeClickButton(response, nextBtn);
138
- if (nextPage) {
139
- response = nextPage;
140
- logMsg(response, 'shop-page2');
141
- // Try finding button again
142
- buyBtn = getAllButtons(response).find(b => b.label && b.label.toLowerCase().includes(searchName));
143
- }
144
- } catch (e) {
145
- LOG.error(`Page nav failed: ${e.message}`);
146
- }
137
+ LOG.debug(`Available: ${allBtns.map(b => `"${b.label}"`).join(', ')}`);
147
138
  }
148
139
 
149
140
  if (!buyBtn) {
package/lib/grinder.js CHANGED
@@ -509,6 +509,7 @@ class AccountWorker {
509
509
  this.globalCooldownUntil = 0;
510
510
  this.commandQueue = null;
511
511
  this.lastHealthCheck = Date.now();
512
+ this.doneToday = new Map(); // in-memory dedup: cmd → expiry timestamp
512
513
  }
513
514
 
514
515
  get tag() { return `${this.color}${c.bold}${this.username}${c.reset}`; }
@@ -616,8 +617,8 @@ class AccountWorker {
616
617
  return null;
617
618
  }
618
619
 
619
- async buyItem(itemName, quantity = 10) {
620
- const MAX_RETRIES = 3;
620
+ async buyItem(itemName, quantity = 1) {
621
+ const MAX_RETRIES = 1;
621
622
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
622
623
  this.log('buy', `Opening shop to buy ${c.bold}${quantity}x ${itemName}${c.reset}... (attempt ${attempt}/${MAX_RETRIES})`);
623
624
  if (this.account.use_slash) {
@@ -671,12 +672,18 @@ class AccountWorker {
671
672
  }
672
673
  await humanDelay(1000, 2000);
673
674
 
674
- // Find Buy button
675
+ // Find Buy button — match by full name or partial name
675
676
  let buyBtn = null;
676
- const searchName = itemName.toLowerCase().replace('hunting ', '').replace('fishing ', '');
677
+ const searchNames = [
678
+ itemName.toLowerCase(),
679
+ itemName.toLowerCase().replace('hunting ', '').replace('fishing ', ''),
680
+ itemName.toLowerCase().split(' ')[0],
681
+ ];
677
682
  for (const row of response.components || []) {
678
683
  for (const comp of row.components || []) {
679
- if (comp.type === 2 && comp.label && comp.label.toLowerCase().includes(searchName)) {
684
+ if (comp.type !== 2 || !comp.label) continue;
685
+ const label = comp.label.toLowerCase();
686
+ if (searchNames.some(s => label.includes(s) || s.includes(label))) {
680
687
  buyBtn = comp; break;
681
688
  }
682
689
  }
@@ -788,7 +795,7 @@ class AccountWorker {
788
795
  case 'dep max': cmdString = `${prefix} dep max`; break;
789
796
  case 'with max': cmdString = `${prefix} with max`; break;
790
797
  case 'blackjack': cmdString = `${prefix} bj ${betAmount}`; break;
791
- case 'coinflip': cmdString = `${prefix} coinflip ${betAmount} heads`; break;
798
+ case 'cointoss': cmdString = `${prefix} cointoss ${betAmount}`; break;
792
799
  case 'roulette': cmdString = `${prefix} roulette ${betAmount} red`; break;
793
800
  case 'slots': cmdString = `${prefix} slots ${betAmount}`; break;
794
801
  case 'snakeeyes': cmdString = `${prefix} snakeeyes ${betAmount}`; break;
@@ -826,7 +833,7 @@ class AccountWorker {
826
833
  case 'blackjack': cmdResult = await commands.runBlackjack(cmdOpts); break;
827
834
  case 'trivia': cmdResult = await commands.runTrivia(cmdOpts); break;
828
835
  case 'work shift': cmdResult = await commands.runWorkShift(cmdOpts); break;
829
- case 'coinflip': cmdResult = await commands.runCoinflip(cmdOpts); break;
836
+ case 'cointoss': cmdResult = await commands.runCointoss(cmdOpts); break;
830
837
  case 'roulette': cmdResult = await commands.runRoulette(cmdOpts); break;
831
838
  case 'slots': cmdResult = await commands.runSlots(cmdOpts); break;
832
839
  case 'snakeeyes': cmdResult = await commands.runSnakeeyes(cmdOpts); break;
@@ -878,18 +885,24 @@ class AccountWorker {
878
885
  return;
879
886
  }
880
887
 
881
- // Min bet detection — raise bet amount
882
- if (resultLower.includes('cannot bet less than') || resultLower.includes('minimum bet')) {
883
- const betMatch = result.match(/⏣\s*([\d,]+)/);
888
+ // Min bet detection — "You can't bet less than 10,000" or "cannot bet less than ⏣ 5,000"
889
+ if (resultLower.includes("can't bet less than") || resultLower.includes('cannot bet less than') || resultLower.includes('minimum bet')) {
890
+ const betMatch = result.match(/less than\s*\*?\*?\s*[⏣o]?\s*([\d,]+)/i) || result.match(/(\d[\d,]+)/);
884
891
  if (betMatch) {
885
892
  const minBet = parseInt(betMatch[1].replace(/,/g, ''));
886
893
  if (minBet > 0) {
887
894
  this.account.bet_amount = minBet;
888
- this.log('info', `${cmdName} min bet: ⏣ ${minBet.toLocaleString()}`);
895
+ this.log('info', `${cmdName} min bet raised → ⏣ ${minBet.toLocaleString()}`);
889
896
  }
890
897
  }
891
898
  return;
892
899
  }
900
+ // Also handle newMinBet from gamble handler
901
+ if (cmdResult.newMinBet && cmdResult.newMinBet > (this.account.bet_amount || 0)) {
902
+ this.account.bet_amount = cmdResult.newMinBet;
903
+ this.log('info', `${cmdName} min bet raised → ⏣ ${cmdResult.newMinBet.toLocaleString()}`);
904
+ return;
905
+ }
893
906
 
894
907
  // Premium-only command detection — disable for 24h
895
908
  if (resultLower.includes('only available on premium') || resultLower.includes('premium') ||
@@ -899,41 +912,50 @@ class AccountWorker {
899
912
  return;
900
913
  }
901
914
 
902
- // Already claimed today (daily/weekly) — set long cooldown + mark in Redis
915
+ // Already claimed today (daily/weekly) — set long cooldown + mark done
903
916
  if (resultLower.includes('already got your daily') || resultLower.includes('try again <t:')) {
904
917
  this.log('info', `${cmdName} already claimed — waiting`);
905
918
  const timeMatch = result.match(/<t:(\d+):R>/);
919
+ let waitSec;
906
920
  if (timeMatch) {
907
921
  const nextAvail = parseInt(timeMatch[1]) * 1000;
908
- const waitSec = Math.max(60, Math.ceil((nextAvail - Date.now()) / 1000));
909
- await this.setCooldown(cmdName, waitSec);
910
- if (redis) try { await redis.set(`dkg:done:${this.account.id}:${cmdName}`, '1', 'EX', waitSec); } catch {}
922
+ waitSec = Math.max(60, Math.ceil((nextAvail - Date.now()) / 1000));
911
923
  } else {
912
- const fallbackSec = cmdName === 'daily' ? 86400 : 604800;
913
- await this.setCooldown(cmdName, fallbackSec);
914
- if (redis) try { await redis.set(`dkg:done:${this.account.id}:${cmdName}`, '1', 'EX', fallbackSec); } catch {}
924
+ waitSec = cmdName === 'daily' ? 86400 : 604800;
915
925
  }
926
+ await this.setCooldown(cmdName, waitSec);
927
+ this.doneToday.set(cmdName, Date.now() + waitSec * 1000);
928
+ if (redis) try { await redis.set(`dkg:done:${this.account.id}:${cmdName}`, '1', 'EX', waitSec); } catch {}
916
929
  return;
917
930
  }
918
931
 
919
- // Scratch level-gate: if level too low, disable for 24h
932
+ // Skip reasons: level too low, no ticket, etc.
920
933
  if (cmdResult.skipReason === 'level') {
921
934
  this.log('warn', `${cmdName} level too low — retry in 24h`);
922
935
  await this.setCooldown(cmdName, 86400);
923
936
  return;
924
937
  }
938
+ if (cmdResult.skipReason === 'no_ticket') {
939
+ this.log('warn', `${cmdName} no ticket — retry in 1h`);
940
+ await this.setCooldown(cmdName, 3600);
941
+ return;
942
+ }
925
943
 
926
944
  const earned = Math.max(0, cmdResult.coins || 0);
927
945
  const spent = Math.max(0, cmdResult.lost || 0);
928
946
  if (earned > 0) this.stats.coins += earned;
929
947
  if (cmdResult.nextCooldownSec) await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
930
948
 
931
- // Mark daily/drops as done in Redis so we don't re-run this session
932
- if (redis && earned > 0 && cmdName === 'daily') {
933
- try { await redis.set(`dkg:done:${this.account.id}:daily`, '1', 'EX', 86400); } catch {}
949
+ // Mark daily/drops as done so we don't re-run this session
950
+ if (cmdName === 'daily' && earned > 0) {
951
+ const expiry = Date.now() + 86400 * 1000;
952
+ this.doneToday.set('daily', expiry);
953
+ if (redis) try { await redis.set(`dkg:done:${this.account.id}:daily`, '1', 'EX', 86400); } catch {}
934
954
  }
935
- if (redis && cmdName === 'drops') {
936
- try { await redis.set(`dkg:done:${this.account.id}:drops`, '1', 'EX', 86400); } catch {}
955
+ if (cmdName === 'drops') {
956
+ const expiry = Date.now() + 86400 * 1000;
957
+ this.doneToday.set('drops', expiry);
958
+ if (redis) try { await redis.set(`dkg:done:${this.account.id}:drops`, '1', 'EX', 86400); } catch {}
937
959
  }
938
960
 
939
961
  if (cmdResult.holdTightReason) {
@@ -991,7 +1013,7 @@ class AccountWorker {
991
1013
  { key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 2 },
992
1014
  // Gambling (fast cycle)
993
1015
  { key: 'cmd_blackjack', cmd: 'blackjack', cdKey: 'cd_blackjack', defaultCd: 3, priority: 3 },
994
- { key: 'cmd_cointoss', cmd: 'coinflip', cdKey: 'cd_cointoss', defaultCd: 2, priority: 3 },
1016
+ { key: 'cmd_cointoss', cmd: 'cointoss', cdKey: 'cd_cointoss', defaultCd: 2, priority: 3 },
995
1017
  { key: 'cmd_roulette', cmd: 'roulette', cdKey: 'cd_roulette', defaultCd: 3, priority: 3 },
996
1018
  { key: 'cmd_slots', cmd: 'slots', cdKey: 'cd_slots', defaultCd: 3, priority: 3 },
997
1019
  { key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 3 },
@@ -1147,17 +1169,28 @@ class AccountWorker {
1147
1169
  return;
1148
1170
  }
1149
1171
 
1150
- // Skip daily/drops if already done today (Redis marker)
1151
- if (redis && (item.cmd === 'daily' || item.cmd === 'drops')) {
1152
- try {
1153
- const done = await redis.get(`dkg:done:${this.account.id}:${item.cmd}`);
1154
- if (done) {
1155
- item.nextRunAt = now + 86400 * 1000;
1156
- if (this.commandQueue) this.commandQueue.push(item);
1157
- this.tickTimeout = setTimeout(() => this.tick(), 100);
1158
- return;
1159
- }
1160
- } catch {}
1172
+ // Skip daily/drops if already done today (in-memory + Redis)
1173
+ if (item.cmd === 'daily' || item.cmd === 'drops') {
1174
+ const memExpiry = this.doneToday.get(item.cmd);
1175
+ if (memExpiry && Date.now() < memExpiry) {
1176
+ item.nextRunAt = memExpiry;
1177
+ if (this.commandQueue) this.commandQueue.push(item);
1178
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
1179
+ return;
1180
+ }
1181
+ if (redis) {
1182
+ try {
1183
+ const done = await redis.get(`dkg:done:${this.account.id}:${item.cmd}`);
1184
+ if (done) {
1185
+ const expiry = now + 86400 * 1000;
1186
+ this.doneToday.set(item.cmd, expiry);
1187
+ item.nextRunAt = expiry;
1188
+ if (this.commandQueue) this.commandQueue.push(item);
1189
+ this.tickTimeout = setTimeout(() => this.tick(), 100);
1190
+ return;
1191
+ }
1192
+ } catch {}
1193
+ }
1161
1194
  }
1162
1195
 
1163
1196
  this.busy = true;
@@ -1167,10 +1200,11 @@ class AccountWorker {
1167
1200
 
1168
1201
  await this.setCooldown(item.cmd, totalWait);
1169
1202
 
1203
+ // Inter-command delay: 1-3s random (human-like spacing)
1170
1204
  const timeSinceLastCmd = now - (this.lastCommandRun || 0);
1171
- const globalJitter = 500 + Math.random() * 1000;
1172
- if (timeSinceLastCmd < globalJitter) {
1173
- await new Promise(r => setTimeout(r, globalJitter - timeSinceLastCmd));
1205
+ const minGap = 1000 + Math.random() * 2000; // 1-3s
1206
+ if (timeSinceLastCmd < minGap) {
1207
+ await new Promise(r => setTimeout(r, minGap - timeSinceLastCmd));
1174
1208
  }
1175
1209
 
1176
1210
  const prefix = this.account.use_slash ? '/' : 'pls';
@@ -1326,7 +1360,7 @@ class AccountWorker {
1326
1360
  { key: 'cmd_stream', l: 'stream' }, { key: 'cmd_scratch', l: 'scratch' },
1327
1361
  { key: 'cmd_adventure', l: 'adv' }, { key: 'cmd_farm', l: 'farm' },
1328
1362
  { key: 'cmd_tidy', l: 'tidy' }, { key: 'cmd_blackjack', l: 'bj' },
1329
- { key: 'cmd_cointoss', l: 'flip' }, { key: 'cmd_roulette', l: 'roul' },
1363
+ { key: 'cmd_cointoss', l: 'toss' }, { key: 'cmd_roulette', l: 'roul' },
1330
1364
  { key: 'cmd_slots', l: 'slots' }, { key: 'cmd_snakeeyes', l: 'snake' },
1331
1365
  { key: 'cmd_trivia', l: 'trivia' }, { key: 'cmd_use', l: 'use' },
1332
1366
  { key: 'cmd_deposit', l: 'dep' }, { key: 'cmd_drops', l: 'drops' },
@@ -1377,7 +1411,7 @@ async function start(apiKey, apiUrl) {
1377
1411
 
1378
1412
  console.log(colorBanner());
1379
1413
  console.log(
1380
- ` ${rgb(139, 92, 246)}v4.6${c.reset}` +
1414
+ ` ${rgb(139, 92, 246)}v4.8${c.reset}` +
1381
1415
  ` ${c.dim}·${c.reset} ${c.white}30 Commands${c.reset}` +
1382
1416
  ` ${c.dim}·${c.reset} ${rgb(34, 211, 238)}Priority Queue${c.reset}` +
1383
1417
  ` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}Redis Cooldowns${c.reset}` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "4.6.0",
3
+ "version": "4.8.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"