dankgrinder 8.107.0 → 8.109.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.
@@ -16,7 +16,7 @@ const { runPostMemes } = require('./postmemes');
16
16
  const { runScratch } = require('./scratch');
17
17
  const { runBlackjack } = require('./blackjack');
18
18
  const { runTrivia, triviaDB } = require('./trivia');
19
- const { runWorkShift } = require('./work');
19
+ const { runWorkShift, autoApplyForJob } = require('./work');
20
20
  const { runCointoss, runRoulette, runSlots, runSnakeeyes } = require('./gamble');
21
21
  const { runDeposit } = require('./deposit');
22
22
  const { runGeneric, runAlert } = require('./generic');
@@ -44,6 +44,7 @@ module.exports = {
44
44
  runBlackjack,
45
45
  runTrivia,
46
46
  runWorkShift,
47
+ autoApplyForJob,
47
48
  runCointoss,
48
49
  runRoulette,
49
50
  runSlots,
package/lib/grinder.js CHANGED
@@ -670,9 +670,10 @@ class AccountWorker {
670
670
  this._levelQuestActive = false;
671
671
  this._levelQuestQueue = [];
672
672
  this._levelQuestDone = new Set();
673
- this._levelQuestProgressCache = new Map();
673
+ this._levelQuestProgressCache = new Map();
674
+ this._levelQuestRunMap = new Map(); // level -> { startedAt, updatedAt, entries: Map<cmd, state> }
674
675
  this._questBetOverride = null;
675
- this._lastCommandMeta = null;
676
+ this._lastCommandMeta = null;
676
677
  this._commandRunning = false; // prevents grinding commands from overlapping with quest commands
677
678
  this._verifyLevelUnlock = null; // holds level to verify after quest completion
678
679
  this.commandQueue = null;
@@ -843,148 +844,13 @@ class AccountWorker {
843
844
  }
844
845
 
845
846
  async buyItem(itemName, quantity = 1) {
846
- const MAX_RETRIES = 1;
847
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
848
- this.log('buy', `Opening shop to buy ${c.bold}${quantity}x ${itemName}${c.reset}... (attempt ${attempt}/${MAX_RETRIES})`);
849
- if (this.account.use_slash) {
850
- await this.channel.sendSlash(DANK_MEMER_ID, 'shop', 'view').catch(() => this.channel.send('pls shop view'));
851
- } else {
852
- await this.channel.send('pls shop view');
853
- }
854
- let response = await this.waitForDankMemer(10000);
855
- if (!response) {
856
- this.log('warn', 'No response to shop view command.');
857
- if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
858
- return false;
859
- }
860
-
861
- const responseText = getFullText(response).toLowerCase();
862
- const hasShopComponents = (response.components || []).some(row =>
863
- (row.components || []).some(comp => comp.type === 3 || (comp.type === 2 && comp.label && comp.label.toLowerCase().includes('buy')))
864
- );
865
-
866
- if (!hasShopComponents && (responseText.includes('lucky') || responseText.includes('event') || responseText.includes('for the rest of the day'))) {
867
- this.log('warn', 'Got event response instead of shop. Retrying...');
868
- await humanDelay(3000, 5000);
869
- continue;
870
- }
871
- if (!hasShopComponents && responseText.includes('shop')) {
872
- const shopUI = await this.waitForDankMemer(8000);
873
- if (shopUI) response = shopUI;
874
- }
875
-
876
- // Navigate to Coin Shop
877
- let coinShopMenuId = null;
878
- let coinShopOption = null;
879
- for (const row of response.components || []) {
880
- for (const comp of row.components || []) {
881
- if (comp.type === 3) {
882
- const opt = (comp.options || []).find(o => o.label && o.label.includes('Coin Shop'));
883
- if (opt) { coinShopMenuId = comp.customId; coinShopOption = opt; }
884
- }
885
- }
886
- }
887
-
888
- if (coinShopMenuId && coinShopOption) {
889
- this.log('buy', 'Navigating to Coin Shop...');
890
- try {
891
- await response.selectMenu(coinShopMenuId, [coinShopOption.value]);
892
- const updatedMsg = await this.waitForDankMemer(8000);
893
- if (updatedMsg) response = updatedMsg;
894
- } catch (e) {
895
- this.log('error', `Failed to open Coin Shop: ${e.message}`);
896
- }
897
- }
898
- await humanDelay(300, 600);
899
-
900
- // Find Buy button — match by full name or partial name
901
- let buyBtn = null;
902
- const searchNames = [
903
- itemName.toLowerCase(),
904
- itemName.toLowerCase().replace('hunting ', '').replace('fishing ', ''),
905
- itemName.toLowerCase().split(' ')[0],
906
- ];
907
- for (const row of response.components || []) {
908
- for (const comp of row.components || []) {
909
- if (comp.type !== 2 || !comp.label) continue;
910
- const label = comp.label.toLowerCase();
911
- if (searchNames.some(s => label.includes(s) || s.includes(label))) {
912
- buyBtn = comp; break;
913
- }
914
- }
915
- if (buyBtn) break;
916
- }
917
-
918
- if (!buyBtn) {
919
- this.log('warn', `Could not find Buy button for ${itemName} (attempt ${attempt})`);
920
- if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
921
- return false;
922
- }
923
-
924
- this.log('buy', `Clicking Buy ${itemName}...`);
925
- try { await safeClickButton(response, buyBtn); } catch (e) {
926
- this.log('error', `Buy click failed: ${e.message}`);
927
- if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
928
- return false;
929
- }
930
-
931
- // Handle Modal
932
- const modal = await new Promise((resolve) => {
933
- const timer = setTimeout(() => resolve(null), 8000);
934
- const handler = (m) => {
935
- clearTimeout(timer);
936
- this.client.removeListener('interactionModalCreate', handler);
937
- resolve(m);
938
- };
939
- this.client.on('interactionModalCreate', handler);
940
- });
941
-
942
- if (modal) {
943
- this.log('buy', `Submitting quantity ${c.bold}${quantity}${c.reset} in modal...`);
944
- try {
945
- const quantityInputId = modal.components[0].components[0].customId;
946
- await fetch('https://discord.com/api/v9/interactions', {
947
- method: 'POST',
948
- headers: { 'Authorization': this.client.token, 'Content-Type': 'application/json' },
949
- body: JSON.stringify({
950
- type: 5, application_id: modal.applicationId,
951
- channel_id: this.channel.id, guild_id: this.channel.guild?.id,
952
- data: {
953
- id: modal.id, custom_id: modal.customId,
954
- components: [{ type: 1, components: [{ type: 4, custom_id: quantityInputId, value: String(quantity) }] }]
955
- },
956
- session_id: this.client.sessionId || "dummy_session",
957
- nonce: Date.now().toString()
958
- })
959
- });
960
- } catch (e) {
961
- this.log('error', `Modal submit failed: ${e.message}`);
962
- if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
963
- return false;
964
- }
965
- } else {
966
- this.log('warn', 'No modal appeared after clicking buy.');
967
- if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
968
- return false;
969
- }
970
-
971
- const confirmMsg = await this.waitForDankMemer(8000);
972
- if (confirmMsg) {
973
- const text = getFullText(confirmMsg).toLowerCase();
974
- if (text.includes('bought') || text.includes('purchased') || text.includes('success')) {
975
- this.log('success', `Bought ${c.bold}${quantity}x ${itemName}${c.reset}!`);
976
- return true;
977
- }
978
- if (text.includes('not enough') || text.includes("can't afford") || text.includes('insufficient')) {
979
- this.log('warn', `Not enough coins to buy ${itemName}.`);
980
- return false;
981
- }
982
- }
983
- this.log('success', `Submitted purchase for ${quantity}x ${itemName}.`);
984
- return true;
985
- }
986
- this.log('error', `Failed to buy ${itemName} after ${MAX_RETRIES} attempts.`);
987
- return false;
847
+ return !!(await commands.buyItem({
848
+ channel: this.channel,
849
+ waitForDankMemer: (timeout) => this.waitForDankMemer(timeout),
850
+ client: this.client,
851
+ itemName,
852
+ quantity,
853
+ }));
988
854
  }
989
855
 
990
856
  // ── Check Balance ───────────────────────────────────────────
@@ -1387,7 +1253,7 @@ class AccountWorker {
1387
1253
  // Each modular command handler sends the command, waits for response,
1388
1254
  // handles Hold Tight / cooldowns / item-buying internally.
1389
1255
  async runCommand(cmdName, prefix) {
1390
- this._lastCommandMeta = { cmdName, nonPlay: false, spent: 0, earned: 0, holdTightReason: null, newMinBet: null, levelLocked: null };
1256
+ this._lastCommandMeta = { cmdName, nonPlay: false, spent: 0, earned: 0, holdTightReason: null, newMinBet: null, levelLocked: null, nextRetrySec: 0 };
1391
1257
  let cmdString;
1392
1258
  const bjBet = Math.max(5000, this.account.bet_amount || 5000);
1393
1259
  const gambBet = Math.max(10000, this.account.bet_amount || 10000);
@@ -1502,7 +1368,7 @@ class AccountWorker {
1502
1368
  case 'search': cmdResult = await commands.runSearch(cmdOpts); break;
1503
1369
  case 'crime': cmdResult = await commands.runCrime(cmdOpts); break;
1504
1370
  case 'hl': cmdResult = await commands.runHighLow(cmdOpts); break;
1505
- case 'farm': cmdResult = await commands.runFarm(cmdOpts); break;
1371
+ case 'farm': cmdResult = await commands.runFarm(cmdOpts); break;
1506
1372
  case 'pm': cmdResult = await commands.runPostMemes(cmdOpts); break;
1507
1373
  case 'hunt': cmdResult = await commands.runHunt(cmdOpts); break;
1508
1374
  case 'dig': cmdResult = await commands.runDig(cmdOpts); break;
@@ -1512,6 +1378,20 @@ class AccountWorker {
1512
1378
  case 'blackjack': cmdResult = await commands.runBlackjack(cmdOpts); break;
1513
1379
  case 'trivia': cmdResult = await commands.runTrivia(cmdOpts); break;
1514
1380
  case 'work shift': cmdResult = await commands.runWorkShift(cmdOpts); break;
1381
+ case 'work apply': {
1382
+ const ar = await commands.autoApplyForJob(cmdOpts);
1383
+ cmdResult = ar?.applied
1384
+ ? { result: 'work apply completed', coins: 0, nextCooldownSec: 8 }
1385
+ : { result: `work apply failed${ar?.cooldownSec ? ` (${Math.ceil(ar.cooldownSec / 60)}m)` : ''}`, coins: 0, nonPlay: true, nextCooldownSec: ar?.cooldownSec || 60 };
1386
+ break;
1387
+ }
1388
+ case 'shop view': {
1389
+ const bought = await commands.buyItem({ ...cmdOpts, itemName: 'Shovel', quantity: 1 });
1390
+ cmdResult = bought
1391
+ ? { result: 'shop buy item completed', coins: 0, nextCooldownSec: 10 }
1392
+ : { result: 'shop buy item failed', coins: 0, nonPlay: true, nextCooldownSec: 25 };
1393
+ break;
1394
+ }
1515
1395
  case 'cointoss': cmdResult = await commands.runCointoss(cmdOpts); break;
1516
1396
  case 'roulette': cmdResult = await commands.runRoulette(cmdOpts); break;
1517
1397
  case 'slots': cmdResult = await commands.runSlots(cmdOpts); break;
@@ -1526,6 +1406,7 @@ class AccountWorker {
1526
1406
 
1527
1407
  const result = cmdResult.result || 'done';
1528
1408
  const resultLower = result.toLowerCase();
1409
+ this._lastCommandMeta.result = result;
1529
1410
 
1530
1411
  // Rate limit detection — progressive backoff based on frequency
1531
1412
  if (resultLower.includes('slow down') || resultLower.includes('rate limit') || resultLower.includes('too fast')) {
@@ -1535,6 +1416,7 @@ class AccountWorker {
1535
1416
  const cooldownSec = Math.min(30 * Math.pow(2, Math.min(this._rateLimitHits - 1, 3)), 300);
1536
1417
  this.log('warn', `Rate limited! ${cooldownSec}s cooldown (hit #${this._rateLimitHits})`);
1537
1418
  this.globalCooldownUntil = Date.now() + cooldownSec * 1000;
1419
+ this._lastCommandMeta.nextRetrySec = cooldownSec;
1538
1420
  await this.setCooldown(cmdName, cooldownSec);
1539
1421
  // Reset rate limit count after 10 minutes of no hits
1540
1422
  setTimeout(() => { if (this._rateLimitHits > 0) this._rateLimitHits = Math.max(0, this._rateLimitHits - 1); }, 600_000);
@@ -1545,6 +1427,8 @@ class AccountWorker {
1545
1427
  if (resultLower.includes('cannot post another meme') || resultLower.includes('dead meme')) {
1546
1428
  const minMatch = result.match(/(\d+)\s*minute/i);
1547
1429
  const cdSec = minMatch ? parseInt(minMatch[1]) * 60 + 30 : 150; // dead meme = N min + 30s buffer
1430
+ this._lastCommandMeta.nonPlay = true;
1431
+ this._lastCommandMeta.nextRetrySec = cdSec;
1548
1432
  this.log('warn', `${cmdName} on cooldown: ${cdSec}s`);
1549
1433
  await this.setCooldown(cmdName, cdSec);
1550
1434
  return;
@@ -1565,6 +1449,28 @@ class AccountWorker {
1565
1449
  this._mergeLevelQuestProgress(targetLv, progress);
1566
1450
  }
1567
1451
  this.log('warn', `Command /${cmdName} requires Level ${targetLv} quests`);
1452
+ } else {
1453
+ // Fallback: some responses are truncated/ephemeral and omit "Level X" in result text.
1454
+ // Run one profile check and infer lock level from raw profile text.
1455
+ try {
1456
+ const profile = await this.checkProfile(true);
1457
+ const profileText = String(profile?.rawText || '');
1458
+ if (profileText.toLowerCase().includes('not unlocked') || profileText.toLowerCase().includes('have not unlocked')) {
1459
+ const pm = profileText.match(/level\s*(\d+)/i);
1460
+ if (pm) {
1461
+ const targetLv = parseInt(pm[1], 10);
1462
+ if (Number.isFinite(targetLv) && targetLv > 0) {
1463
+ cmdResult.levelLocked = targetLv;
1464
+ const progress = this._parseLevelQuestProgress(targetLv, profileText);
1465
+ if (progress) {
1466
+ cmdResult.levelQuestProgress = progress;
1467
+ this._mergeLevelQuestProgress(targetLv, progress);
1468
+ }
1469
+ this.log('warn', `Command /${cmdName} requires Level ${targetLv} quests (profile fallback)`);
1470
+ }
1471
+ }
1472
+ }
1473
+ } catch {}
1568
1474
  }
1569
1475
  }
1570
1476
 
@@ -1660,6 +1566,7 @@ class AccountWorker {
1660
1566
  if (resultLower.includes('already got your daily') || resultLower.includes('already got your weekly') ||
1661
1567
  resultLower.includes('already got your monthly') || resultLower.includes('already claimed') ||
1662
1568
  resultLower.includes('try again <t:')) {
1569
+ this._lastCommandMeta.nonPlay = true;
1663
1570
  this.log('info', `${cmdName} already claimed — waiting`);
1664
1571
  const timeMatch = result.match(/<t:(\d+):R>/);
1665
1572
  let waitSec;
@@ -1670,6 +1577,7 @@ class AccountWorker {
1670
1577
  const defaultWaits = { daily: 86400, weekly: 604800, monthly: 2592000 };
1671
1578
  waitSec = defaultWaits[cmdName] || 86400;
1672
1579
  }
1580
+ this._lastCommandMeta.nextRetrySec = waitSec;
1673
1581
  await this.setCooldown(cmdName, waitSec);
1674
1582
  this.doneToday.set(cmdName, Date.now() + waitSec * 1000);
1675
1583
  if (redis) try { await redis.set(`dkg:done:${this.account.id}:${cmdName}`, '1', 'EX', waitSec); } catch {}
@@ -1695,6 +1603,7 @@ class AccountWorker {
1695
1603
  // Track net earnings (add wins, subtract losses)
1696
1604
  this.stats.coins += (earned - spent);
1697
1605
  if (cmdResult.nextCooldownSec) {
1606
+ this._lastCommandMeta.nextRetrySec = Math.max(this._lastCommandMeta.nextRetrySec || 0, cmdResult.nextCooldownSec);
1698
1607
  await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
1699
1608
  this._lastCooldownOverride = cmdResult.nextCooldownSec;
1700
1609
  // Learn: record this cooldown as the known value for future fallback use
@@ -1738,6 +1647,7 @@ class AccountWorker {
1738
1647
  this._lastCommandMeta.nonPlay = true;
1739
1648
  this._lastCommandMeta.holdTightReason = reason;
1740
1649
  const holdSec = 35;
1650
+ this._lastCommandMeta.nextRetrySec = Math.max(this._lastCommandMeta.nextRetrySec || 0, holdSec);
1741
1651
  this.log('warn', `Hold Tight: /${reason} — ${holdSec}s global cooldown`);
1742
1652
  const reasonMap = { postmemes: 'pm', highlow: 'hl', blackjack: 'bj', 'work shift': 'work shift' };
1743
1653
  const mappedCmd = reasonMap[reason] || reason;
@@ -1917,10 +1827,21 @@ class AccountWorker {
1917
1827
  { cmd: 'search', times: 2 },
1918
1828
  { cmd: 'tidy', times: 2 },
1919
1829
  ],
1830
+ 2: [
1831
+ { cmd: 'inventory', times: 1, progressMatch: 'inventory', progressSlash: 'inventory' },
1832
+ { cmd: 'balance', times: 1, progressMatch: 'balance', progressSlash: 'balance' },
1833
+ { cmd: 'hunt', times: 2, progressMatch: 'hunt', progressSlash: 'hunt' },
1834
+ { cmd: 'dig', times: 2, progressMatch: 'dig', progressSlash: 'dig' },
1835
+ ],
1920
1836
  3: [
1921
- { cmd: 'work apply', times: 1 },
1922
- { cmd: 'work shift', times: 1 },
1923
- { cmd: 'shop sell common coin 1', times: 2 },
1837
+ { cmd: 'work apply', times: 1, progressMatch: 'get a job', progressSlash: 'work apply' },
1838
+ { cmd: 'work shift', times: 1, progressMatch: 'work a shift', progressSlash: 'work shift' },
1839
+ { cmd: 'shop sell item shovel 1',times: 2, progressMatch: 'sell items', progressSlash: 'shop sell' },
1840
+ { cmd: 'shop view', times: 1, progressMatch: 'buy an item', progressSlash: 'shop view' },
1841
+ ],
1842
+ 4: [
1843
+ // Keep this command-driven so the quest runner can continue progression.
1844
+ { cmd: 'bal', times: 1, progressMatch: 'balance', progressSlash: 'balance' },
1924
1845
  ],
1925
1846
  5: [
1926
1847
  // "Lose 50,000 coins in /slots" etc — loseTarget means keep playing until cumulative losses hit 50k
@@ -1928,24 +1849,89 @@ class AccountWorker {
1928
1849
  { cmd: 'cointoss', loseTarget: 50000, bet: 10000, times: 50 },
1929
1850
  { cmd: 'snakeeyes', loseTarget: 50000, bet: 10000, times: 50 },
1930
1851
  ],
1852
+ 6: [
1853
+ // Slash text in lock-card maps to regular pls command strings below.
1854
+ { cmd: 'item new player pack', times: 1, progressMatch: 'new player pack', progressSlash: 'item' },
1855
+ { cmd: 'use player 1', times: 1, progressMatch: 'new player pack', progressSlash: 'use' },
1856
+ { cmd: 'use normie 1', times: 1, progressMatch: 'normie box', progressSlash: 'use' },
1857
+ ],
1858
+ 7: [
1859
+ { cmd: 'title set newbie', times: 1, progressMatch: 'newbie title', progressSlash: 'title' },
1860
+ { cmd: 'profile', times: 1, progressMatch: 'profile', progressSlash: 'profile' },
1861
+ { cmd: 'daily', times: 1, progressMatch: 'daily', progressSlash: 'daily' },
1862
+ { cmd: 'hl', times: 3, progressMatch: 'highlow', progressSlash: 'highlow' },
1863
+ ],
1931
1864
  };
1932
1865
 
1933
1866
  _parseLevelQuestProgress(targetLevel, rawText) {
1934
- if (targetLevel !== 5) return null;
1935
1867
  const text = String(rawText || '');
1936
1868
  const out = {};
1937
- const re = /(\d[\d,]*)\s*\/\s*50,000[\s\S]{0,180}?<\/(slots|cointoss|snakeeyes):/gi;
1938
- let m;
1939
- while ((m = re.exec(text)) !== null) {
1940
- const cur = parseInt(String(m[1] || '').replace(/,/g, ''), 10);
1941
- const cmd = String(m[2] || '').toLowerCase();
1942
- if (Number.isFinite(cur) && cmd) out[cmd] = Math.max(0, cur);
1869
+
1870
+ if (targetLevel === 5) {
1871
+ const re = /(\d[\d,]*)\s*\/\s*50,000[\s\S]{0,180}?<\/(slots|cointoss|snakeeyes):/gi;
1872
+ let m;
1873
+ while ((m = re.exec(text)) !== null) {
1874
+ const cur = parseInt(String(m[1] || '').replace(/,/g, ''), 10);
1875
+ const cmd = String(m[2] || '').toLowerCase();
1876
+ if (Number.isFinite(cur) && cmd) out[cmd] = Math.max(0, cur);
1877
+ }
1878
+ }
1879
+
1880
+ if (targetLevel === 6) {
1881
+ // handled below by generic times-based parser
1943
1882
  }
1883
+
1884
+ if (targetLevel !== 5) {
1885
+ const quests = AccountWorker.LEVEL_QUESTS[targetLevel] || [];
1886
+ const lines = text.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
1887
+ for (const q of quests) {
1888
+ if (q.loseTarget) continue;
1889
+ const cmd = String(q.cmd || '').toLowerCase();
1890
+ const cmdParts = cmd.split(/\s+/).filter(Boolean);
1891
+ if (cmdParts.length === 0) continue;
1892
+
1893
+ const slashHints = new Set();
1894
+ slashHints.add(cmdParts[0]);
1895
+ if (q.progressSlash) {
1896
+ for (const p of String(q.progressSlash).toLowerCase().split(/[\s,/|]+/).filter(Boolean)) {
1897
+ slashHints.add(p);
1898
+ }
1899
+ }
1900
+ if (cmd === 'hl') slashHints.add('highlow');
1901
+ if (cmd === 'highlow') slashHints.add('hl');
1902
+
1903
+ const matchPhrase = String(q.progressMatch || q.cmd || '').toLowerCase();
1904
+ const tail = matchPhrase
1905
+ .split(/\s+/)
1906
+ .filter(Boolean)
1907
+ .filter(t => !/^\d+$/.test(t))
1908
+ .filter(t => t !== cmdParts[0]);
1909
+
1910
+ for (const line of lines) {
1911
+ const lower = line.toLowerCase();
1912
+ const hasSlash = Array.from(slashHints).some(h => lower.includes(`/${h}`));
1913
+ if (!hasSlash) continue;
1914
+
1915
+ let ok = true;
1916
+ for (const token of tail) {
1917
+ if (!lower.includes(token)) { ok = false; break; }
1918
+ }
1919
+ if (!ok) continue;
1920
+
1921
+ const m = lower.match(/(\d[\d,]*)\s*\/\s*(\d[\d,]*)/);
1922
+ if (!m) continue;
1923
+ const cur = parseInt(String(m[1] || '').replace(/,/g, ''), 10);
1924
+ if (!Number.isFinite(cur)) continue;
1925
+ out[cmd] = Math.max(out[cmd] || 0, cur);
1926
+ }
1927
+ }
1928
+ }
1929
+
1944
1930
  return Object.keys(out).length > 0 ? out : null;
1945
1931
  }
1946
1932
 
1947
1933
  _mergeLevelQuestProgress(targetLevel, progressHint) {
1948
- if (!progressHint || targetLevel !== 5) return;
1934
+ if (!progressHint) return;
1949
1935
  const prev = this._levelQuestProgressCache.get(targetLevel) || {};
1950
1936
  const merged = { ...prev };
1951
1937
  for (const [cmd, val] of Object.entries(progressHint)) {
@@ -1963,13 +1949,31 @@ class AccountWorker {
1963
1949
 
1964
1950
  this._mergeLevelQuestProgress(targetLevel, progressHint);
1965
1951
  const cached = this._levelQuestProgressCache.get(targetLevel) || {};
1966
- const seededQueue = quests
1952
+ const seededAll = quests
1967
1953
  .map(q => {
1968
1954
  const target = Number(q.loseTarget || 0);
1969
- const seeded = target > 0 ? Math.min(target, Math.max(0, Number(cached[q.cmd] || 0))) : 0;
1970
- return { ...q, level: targetLevel, _lostSoFar: seeded };
1971
- })
1972
- .filter(q => !(q.loseTarget && q._lostSoFar >= q.loseTarget));
1955
+ const seededLoss = target > 0 ? Math.min(target, Math.max(0, Number(cached[q.cmd] || 0))) : 0;
1956
+ const seededDone = target === 0 ? Math.max(0, Math.min(Number(q.times || 0), Number(cached[q.cmd] || 0))) : 0;
1957
+ const timesRemaining = target === 0 ? Math.max(0, Number(q.times || 0) - seededDone) : Number(q.times || 0);
1958
+ const defaultCdSec = this._getCommandDefaultCooldownSec(q.cmd);
1959
+ return {
1960
+ ...q,
1961
+ level: targetLevel,
1962
+ _lostSoFar: seededLoss,
1963
+ _doneSoFar: seededDone,
1964
+ times: timesRemaining,
1965
+ _defaultCdSec: defaultCdSec,
1966
+ _nextRunAt: 0,
1967
+ };
1968
+ });
1969
+
1970
+ const seededQueue = seededAll
1971
+ .filter(q => {
1972
+ if (q.loseTarget) return q._lostSoFar < q.loseTarget;
1973
+ return q.times > 0;
1974
+ });
1975
+
1976
+ this._initLevelQuestRunState(targetLevel, seededAll);
1973
1977
 
1974
1978
  if (seededQueue.length === 0) {
1975
1979
  this.log('info', `[QUEST] Level ${targetLevel} tasks already complete from progress cache — verifying unlock`);
@@ -1997,6 +2001,71 @@ class AccountWorker {
1997
2001
  return true;
1998
2002
  }
1999
2003
 
2004
+ _getCommandDefaultCooldownSec(cmdName) {
2005
+ const cmd = String(cmdName || '').toLowerCase().trim();
2006
+ if (!cmd) return 8;
2007
+ const map = AccountWorker.COMMAND_MAP || [];
2008
+ const exact = map.find(r => String(r.cmd || '').toLowerCase() === cmd);
2009
+ if (exact && Number.isFinite(exact.defaultCd) && exact.defaultCd > 0) return exact.defaultCd;
2010
+ if (cmd === 'highlow') {
2011
+ const hl = map.find(r => String(r.cmd || '').toLowerCase() === 'hl');
2012
+ if (hl?.defaultCd) return hl.defaultCd;
2013
+ }
2014
+ const head = cmd.split(/\s+/)[0];
2015
+ const byHead = map.find(r => String(r.cmd || '').toLowerCase() === head);
2016
+ if (byHead && Number.isFinite(byHead.defaultCd) && byHead.defaultCd > 0) return byHead.defaultCd;
2017
+ return 10;
2018
+ }
2019
+
2020
+ _initLevelQuestRunState(targetLevel, questRows) {
2021
+ const startedAt = Date.now();
2022
+ const state = {
2023
+ level: targetLevel,
2024
+ startedAt,
2025
+ updatedAt: startedAt,
2026
+ completedAt: 0,
2027
+ entries: new Map(),
2028
+ };
2029
+
2030
+ for (const q of (questRows || [])) {
2031
+ const cmd = String(q.cmd || '').toLowerCase();
2032
+ if (!cmd) continue;
2033
+ const required = q.loseTarget
2034
+ ? Number(q.loseTarget || 0)
2035
+ : Math.max(0, Number(q._doneSoFar || 0) + Number(q.times || 0));
2036
+ const completed = q.loseTarget
2037
+ ? Math.max(0, Number(q._lostSoFar || 0))
2038
+ : Math.max(0, Number(q._doneSoFar || 0));
2039
+ const remaining = q.loseTarget
2040
+ ? Math.max(0, required - completed)
2041
+ : Math.max(0, Number(q.times || 0));
2042
+
2043
+ state.entries.set(cmd, {
2044
+ cmd,
2045
+ required,
2046
+ completed,
2047
+ remaining,
2048
+ attemptsLeft: Number(q.times || 0),
2049
+ runs: 0,
2050
+ success: 0,
2051
+ failed: 0,
2052
+ nonPlay: 0,
2053
+ lastResult: '',
2054
+ lastRunAt: 0,
2055
+ nextRunAt: Number(q._nextRunAt || 0),
2056
+ status: remaining <= 0 ? 'done' : 'pending',
2057
+ });
2058
+ }
2059
+
2060
+ this._levelQuestRunMap.set(targetLevel, state);
2061
+ }
2062
+
2063
+ _getLevelQuestRunEntry(level, cmdName) {
2064
+ const st = this._levelQuestRunMap.get(level);
2065
+ if (!st?.entries) return null;
2066
+ return st.entries.get(String(cmdName || '').toLowerCase()) || null;
2067
+ }
2068
+
2000
2069
  async buildCommandQueue() {
2001
2070
  const heap = new MinHeap();
2002
2071
  const now = Date.now();
@@ -2259,6 +2328,11 @@ class AccountWorker {
2259
2328
  this.log('success', `[QUEST] Level ${targetLv} verified ✓ — level unlocked!`);
2260
2329
  this._level = currentLv;
2261
2330
  this._levelQuestProgressCache.delete(targetLv);
2331
+ const st = this._levelQuestRunMap.get(targetLv);
2332
+ if (st) {
2333
+ st.updatedAt = Date.now();
2334
+ st.verifiedAt = Date.now();
2335
+ }
2262
2336
  } else {
2263
2337
  this.log('warn', `[QUEST] Level ${targetLv} NOT verified — still at ${currentLv}. Re-triggering quests.`);
2264
2338
  // Re-trigger quests for this level
@@ -2279,7 +2353,22 @@ class AccountWorker {
2279
2353
 
2280
2354
  // BLOCK: quest mode active — run quests only, no normal grinding
2281
2355
  if (this._levelQuestActive && this._levelQuestQueue.length > 0) {
2356
+ const now = Date.now();
2357
+ let dueIdx = this._levelQuestQueue.findIndex(q => !q._nextRunAt || q._nextRunAt <= now);
2358
+ if (dueIdx < 0) {
2359
+ const soonest = Math.min(...this._levelQuestQueue.map(q => Number(q._nextRunAt || now + 5000)));
2360
+ const waitMs = Math.max(1200, Math.min(60000, soonest - now));
2361
+ this.setStatus(`[QUEST] cooldown wait ${Math.ceil(waitMs / 1000)}s`);
2362
+ this.tickTimeout = setTimeout(() => this.tick(), waitMs);
2363
+ return;
2364
+ }
2365
+ if (dueIdx > 0) {
2366
+ const [dueQuest] = this._levelQuestQueue.splice(dueIdx, 1);
2367
+ this._levelQuestQueue.unshift(dueQuest);
2368
+ }
2369
+
2282
2370
  const quest = this._levelQuestQueue[0];
2371
+ let nextQuestDelayMs = 1200;
2283
2372
  if (quest.loseTarget) {
2284
2373
  const remaining = Math.max(0, quest.loseTarget - (quest._lostSoFar || 0));
2285
2374
  const floorBet = quest._minBet || quest.bet || 1;
@@ -2306,6 +2395,12 @@ class AccountWorker {
2306
2395
  this.busy = true;
2307
2396
  await this.runCommand(quest.cmd, prefix);
2308
2397
  const questMeta = this._lastCommandMeta || {};
2398
+ const qEntry = this._getLevelQuestRunEntry(quest.level, quest.cmd);
2399
+ if (qEntry) {
2400
+ qEntry.runs++;
2401
+ qEntry.lastRunAt = Date.now();
2402
+ qEntry.lastResult = String(questMeta.result || '');
2403
+ }
2309
2404
  this._commandRunning = false;
2310
2405
  this.busy = false;
2311
2406
  this._questBetOverride = null;
@@ -2328,6 +2423,10 @@ class AccountWorker {
2328
2423
  if (!nonPlay) {
2329
2424
  quest._lostSoFar = (quest._lostSoFar || 0) + lostThisRound;
2330
2425
  this._mergeLevelQuestProgress(quest.level, { [quest.cmd]: quest._lostSoFar });
2426
+ if (qEntry) qEntry.success++;
2427
+ } else if (qEntry) {
2428
+ qEntry.nonPlay++;
2429
+ qEntry.failed++;
2331
2430
  }
2332
2431
 
2333
2432
  this.log('info', `[QUEST] ${quest.cmd} — lost ⏣${quest._lostSoFar.toLocaleString()} / ⏣${quest.loseTarget.toLocaleString()}`);
@@ -2346,10 +2445,57 @@ class AccountWorker {
2346
2445
  questStepDone = true;
2347
2446
  this.log('info', `[QUEST] ${quest.cmd} max attempts reached — moving on`);
2348
2447
  }
2448
+
2449
+ if (qEntry) {
2450
+ qEntry.completed = Math.max(0, Number(quest._lostSoFar || 0));
2451
+ qEntry.remaining = Math.max(0, Number(quest.loseTarget || 0) - qEntry.completed);
2452
+ qEntry.attemptsLeft = Number(quest.times || 0);
2453
+ qEntry.status = questStepDone ? 'done' : (nonPlay ? 'cooldown' : 'progress');
2454
+ }
2349
2455
  } else {
2350
2456
  // Normal times-based quest
2351
- quest.times--;
2457
+ const nonPlay = !!questMeta.nonPlay;
2458
+ if (!nonPlay) {
2459
+ quest.times--;
2460
+ quest._doneSoFar = (quest._doneSoFar || 0) + 1;
2461
+ this._mergeLevelQuestProgress(quest.level, { [quest.cmd]: quest._doneSoFar });
2462
+ if (qEntry) qEntry.success++;
2463
+ } else {
2464
+ if (qEntry) {
2465
+ qEntry.nonPlay++;
2466
+ qEntry.failed++;
2467
+ }
2468
+ this.log('warn', `[QUEST] ${quest.cmd} non-play response — progress unchanged`);
2469
+ // Rotate to next quest so we don't spam one command during cooldown.
2470
+ if (this._levelQuestQueue.length > 1) {
2471
+ const rotated = this._levelQuestQueue.shift();
2472
+ this._levelQuestQueue.push(rotated);
2473
+ }
2474
+ }
2352
2475
  if (quest.times <= 0) questStepDone = true;
2476
+
2477
+ if (qEntry) {
2478
+ qEntry.completed = Math.max(0, Number(quest._doneSoFar || 0));
2479
+ qEntry.remaining = Math.max(0, Number(quest.times || 0));
2480
+ qEntry.attemptsLeft = Number(quest.times || 0);
2481
+ qEntry.status = questStepDone ? 'done' : (nonPlay ? 'cooldown' : 'progress');
2482
+ }
2483
+ }
2484
+
2485
+ const retrySec = Math.max(
2486
+ 1,
2487
+ Number(questMeta.nextRetrySec || 0),
2488
+ Number(quest._defaultCdSec || 0)
2489
+ );
2490
+ quest._nextRunAt = Date.now() + retrySec * 1000;
2491
+ nextQuestDelayMs = Math.max(nextQuestDelayMs, Math.min(60000, Math.floor(retrySec * 1000)));
2492
+ if (qEntry) qEntry.nextRunAt = Number(quest._nextRunAt || 0);
2493
+
2494
+ const runState = this._levelQuestRunMap.get(quest.level);
2495
+ if (runState) runState.updatedAt = Date.now();
2496
+
2497
+ if (questMeta.nextRetrySec && Number.isFinite(questMeta.nextRetrySec) && questMeta.nextRetrySec > 0) {
2498
+ nextQuestDelayMs = Math.max(nextQuestDelayMs, Math.min(60000, Math.floor(questMeta.nextRetrySec * 1000)));
2353
2499
  }
2354
2500
 
2355
2501
  if (questStepDone) this._levelQuestQueue.shift();
@@ -2358,6 +2504,11 @@ class AccountWorker {
2358
2504
  this._levelQuestActive = false;
2359
2505
  this._levelQuestDone.add(quest.level);
2360
2506
  const justCompletedLevel = quest.level;
2507
+ const st = this._levelQuestRunMap.get(justCompletedLevel);
2508
+ if (st) {
2509
+ st.completedAt = Date.now();
2510
+ st.updatedAt = st.completedAt;
2511
+ }
2361
2512
  this.log('success', `[QUEST] Level ${justCompletedLevel} quests DONE — verifying unlock...`);
2362
2513
  // Null out commandQueue so buildCommandQueue picks only unlocked commands
2363
2514
  this.commandQueue = null;
@@ -2367,7 +2518,7 @@ class AccountWorker {
2367
2518
  this.tickTimeout = setTimeout(() => this.tick(), 3000);
2368
2519
  return;
2369
2520
  }
2370
- this.tickTimeout = setTimeout(() => this.tick(), 2500);
2521
+ this.tickTimeout = setTimeout(() => this.tick(), nextQuestDelayMs);
2371
2522
  return;
2372
2523
  }
2373
2524
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.107.0",
3
+ "version": "8.109.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"