dankgrinder 8.105.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,8 +670,10 @@ class AccountWorker {
670
670
  this._levelQuestActive = false;
671
671
  this._levelQuestQueue = [];
672
672
  this._levelQuestDone = new Set();
673
+ this._levelQuestProgressCache = new Map();
674
+ this._levelQuestRunMap = new Map(); // level -> { startedAt, updatedAt, entries: Map<cmd, state> }
673
675
  this._questBetOverride = null;
674
- this._lastCommandMeta = null;
676
+ this._lastCommandMeta = null;
675
677
  this._commandRunning = false; // prevents grinding commands from overlapping with quest commands
676
678
  this._verifyLevelUnlock = null; // holds level to verify after quest completion
677
679
  this.commandQueue = null;
@@ -842,148 +844,13 @@ class AccountWorker {
842
844
  }
843
845
 
844
846
  async buyItem(itemName, quantity = 1) {
845
- const MAX_RETRIES = 1;
846
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
847
- this.log('buy', `Opening shop to buy ${c.bold}${quantity}x ${itemName}${c.reset}... (attempt ${attempt}/${MAX_RETRIES})`);
848
- if (this.account.use_slash) {
849
- await this.channel.sendSlash(DANK_MEMER_ID, 'shop', 'view').catch(() => this.channel.send('pls shop view'));
850
- } else {
851
- await this.channel.send('pls shop view');
852
- }
853
- let response = await this.waitForDankMemer(10000);
854
- if (!response) {
855
- this.log('warn', 'No response to shop view command.');
856
- if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
857
- return false;
858
- }
859
-
860
- const responseText = getFullText(response).toLowerCase();
861
- const hasShopComponents = (response.components || []).some(row =>
862
- (row.components || []).some(comp => comp.type === 3 || (comp.type === 2 && comp.label && comp.label.toLowerCase().includes('buy')))
863
- );
864
-
865
- if (!hasShopComponents && (responseText.includes('lucky') || responseText.includes('event') || responseText.includes('for the rest of the day'))) {
866
- this.log('warn', 'Got event response instead of shop. Retrying...');
867
- await humanDelay(3000, 5000);
868
- continue;
869
- }
870
- if (!hasShopComponents && responseText.includes('shop')) {
871
- const shopUI = await this.waitForDankMemer(8000);
872
- if (shopUI) response = shopUI;
873
- }
874
-
875
- // Navigate to Coin Shop
876
- let coinShopMenuId = null;
877
- let coinShopOption = null;
878
- for (const row of response.components || []) {
879
- for (const comp of row.components || []) {
880
- if (comp.type === 3) {
881
- const opt = (comp.options || []).find(o => o.label && o.label.includes('Coin Shop'));
882
- if (opt) { coinShopMenuId = comp.customId; coinShopOption = opt; }
883
- }
884
- }
885
- }
886
-
887
- if (coinShopMenuId && coinShopOption) {
888
- this.log('buy', 'Navigating to Coin Shop...');
889
- try {
890
- await response.selectMenu(coinShopMenuId, [coinShopOption.value]);
891
- const updatedMsg = await this.waitForDankMemer(8000);
892
- if (updatedMsg) response = updatedMsg;
893
- } catch (e) {
894
- this.log('error', `Failed to open Coin Shop: ${e.message}`);
895
- }
896
- }
897
- await humanDelay(300, 600);
898
-
899
- // Find Buy button — match by full name or partial name
900
- let buyBtn = null;
901
- const searchNames = [
902
- itemName.toLowerCase(),
903
- itemName.toLowerCase().replace('hunting ', '').replace('fishing ', ''),
904
- itemName.toLowerCase().split(' ')[0],
905
- ];
906
- for (const row of response.components || []) {
907
- for (const comp of row.components || []) {
908
- if (comp.type !== 2 || !comp.label) continue;
909
- const label = comp.label.toLowerCase();
910
- if (searchNames.some(s => label.includes(s) || s.includes(label))) {
911
- buyBtn = comp; break;
912
- }
913
- }
914
- if (buyBtn) break;
915
- }
916
-
917
- if (!buyBtn) {
918
- this.log('warn', `Could not find Buy button for ${itemName} (attempt ${attempt})`);
919
- if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
920
- return false;
921
- }
922
-
923
- this.log('buy', `Clicking Buy ${itemName}...`);
924
- try { await safeClickButton(response, buyBtn); } catch (e) {
925
- this.log('error', `Buy click failed: ${e.message}`);
926
- if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
927
- return false;
928
- }
929
-
930
- // Handle Modal
931
- const modal = await new Promise((resolve) => {
932
- const timer = setTimeout(() => resolve(null), 8000);
933
- const handler = (m) => {
934
- clearTimeout(timer);
935
- this.client.removeListener('interactionModalCreate', handler);
936
- resolve(m);
937
- };
938
- this.client.on('interactionModalCreate', handler);
939
- });
940
-
941
- if (modal) {
942
- this.log('buy', `Submitting quantity ${c.bold}${quantity}${c.reset} in modal...`);
943
- try {
944
- const quantityInputId = modal.components[0].components[0].customId;
945
- await fetch('https://discord.com/api/v9/interactions', {
946
- method: 'POST',
947
- headers: { 'Authorization': this.client.token, 'Content-Type': 'application/json' },
948
- body: JSON.stringify({
949
- type: 5, application_id: modal.applicationId,
950
- channel_id: this.channel.id, guild_id: this.channel.guild?.id,
951
- data: {
952
- id: modal.id, custom_id: modal.customId,
953
- components: [{ type: 1, components: [{ type: 4, custom_id: quantityInputId, value: String(quantity) }] }]
954
- },
955
- session_id: this.client.sessionId || "dummy_session",
956
- nonce: Date.now().toString()
957
- })
958
- });
959
- } catch (e) {
960
- this.log('error', `Modal submit failed: ${e.message}`);
961
- if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
962
- return false;
963
- }
964
- } else {
965
- this.log('warn', 'No modal appeared after clicking buy.');
966
- if (attempt < MAX_RETRIES) { await humanDelay(2000, 4000); continue; }
967
- return false;
968
- }
969
-
970
- const confirmMsg = await this.waitForDankMemer(8000);
971
- if (confirmMsg) {
972
- const text = getFullText(confirmMsg).toLowerCase();
973
- if (text.includes('bought') || text.includes('purchased') || text.includes('success')) {
974
- this.log('success', `Bought ${c.bold}${quantity}x ${itemName}${c.reset}!`);
975
- return true;
976
- }
977
- if (text.includes('not enough') || text.includes("can't afford") || text.includes('insufficient')) {
978
- this.log('warn', `Not enough coins to buy ${itemName}.`);
979
- return false;
980
- }
981
- }
982
- this.log('success', `Submitted purchase for ${quantity}x ${itemName}.`);
983
- return true;
984
- }
985
- this.log('error', `Failed to buy ${itemName} after ${MAX_RETRIES} attempts.`);
986
- 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
+ }));
987
854
  }
988
855
 
989
856
  // ── Check Balance ───────────────────────────────────────────
@@ -1386,7 +1253,7 @@ class AccountWorker {
1386
1253
  // Each modular command handler sends the command, waits for response,
1387
1254
  // handles Hold Tight / cooldowns / item-buying internally.
1388
1255
  async runCommand(cmdName, prefix) {
1389
- 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 };
1390
1257
  let cmdString;
1391
1258
  const bjBet = Math.max(5000, this.account.bet_amount || 5000);
1392
1259
  const gambBet = Math.max(10000, this.account.bet_amount || 10000);
@@ -1501,7 +1368,7 @@ class AccountWorker {
1501
1368
  case 'search': cmdResult = await commands.runSearch(cmdOpts); break;
1502
1369
  case 'crime': cmdResult = await commands.runCrime(cmdOpts); break;
1503
1370
  case 'hl': cmdResult = await commands.runHighLow(cmdOpts); break;
1504
- case 'farm': cmdResult = await commands.runFarm(cmdOpts); break;
1371
+ case 'farm': cmdResult = await commands.runFarm(cmdOpts); break;
1505
1372
  case 'pm': cmdResult = await commands.runPostMemes(cmdOpts); break;
1506
1373
  case 'hunt': cmdResult = await commands.runHunt(cmdOpts); break;
1507
1374
  case 'dig': cmdResult = await commands.runDig(cmdOpts); break;
@@ -1511,6 +1378,20 @@ class AccountWorker {
1511
1378
  case 'blackjack': cmdResult = await commands.runBlackjack(cmdOpts); break;
1512
1379
  case 'trivia': cmdResult = await commands.runTrivia(cmdOpts); break;
1513
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
+ }
1514
1395
  case 'cointoss': cmdResult = await commands.runCointoss(cmdOpts); break;
1515
1396
  case 'roulette': cmdResult = await commands.runRoulette(cmdOpts); break;
1516
1397
  case 'slots': cmdResult = await commands.runSlots(cmdOpts); break;
@@ -1525,6 +1406,7 @@ class AccountWorker {
1525
1406
 
1526
1407
  const result = cmdResult.result || 'done';
1527
1408
  const resultLower = result.toLowerCase();
1409
+ this._lastCommandMeta.result = result;
1528
1410
 
1529
1411
  // Rate limit detection — progressive backoff based on frequency
1530
1412
  if (resultLower.includes('slow down') || resultLower.includes('rate limit') || resultLower.includes('too fast')) {
@@ -1534,6 +1416,7 @@ class AccountWorker {
1534
1416
  const cooldownSec = Math.min(30 * Math.pow(2, Math.min(this._rateLimitHits - 1, 3)), 300);
1535
1417
  this.log('warn', `Rate limited! ${cooldownSec}s cooldown (hit #${this._rateLimitHits})`);
1536
1418
  this.globalCooldownUntil = Date.now() + cooldownSec * 1000;
1419
+ this._lastCommandMeta.nextRetrySec = cooldownSec;
1537
1420
  await this.setCooldown(cmdName, cooldownSec);
1538
1421
  // Reset rate limit count after 10 minutes of no hits
1539
1422
  setTimeout(() => { if (this._rateLimitHits > 0) this._rateLimitHits = Math.max(0, this._rateLimitHits - 1); }, 600_000);
@@ -1544,6 +1427,8 @@ class AccountWorker {
1544
1427
  if (resultLower.includes('cannot post another meme') || resultLower.includes('dead meme')) {
1545
1428
  const minMatch = result.match(/(\d+)\s*minute/i);
1546
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;
1547
1432
  this.log('warn', `${cmdName} on cooldown: ${cdSec}s`);
1548
1433
  await this.setCooldown(cmdName, cdSec);
1549
1434
  return;
@@ -1558,7 +1443,34 @@ class AccountWorker {
1558
1443
  if (lvMatch) {
1559
1444
  const targetLv = parseInt(lvMatch[1]);
1560
1445
  cmdResult.levelLocked = targetLv;
1446
+ const progress = this._parseLevelQuestProgress(targetLv, allDetectionText);
1447
+ if (progress) {
1448
+ cmdResult.levelQuestProgress = progress;
1449
+ this._mergeLevelQuestProgress(targetLv, progress);
1450
+ }
1561
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 {}
1562
1474
  }
1563
1475
  }
1564
1476
 
@@ -1654,6 +1566,7 @@ class AccountWorker {
1654
1566
  if (resultLower.includes('already got your daily') || resultLower.includes('already got your weekly') ||
1655
1567
  resultLower.includes('already got your monthly') || resultLower.includes('already claimed') ||
1656
1568
  resultLower.includes('try again <t:')) {
1569
+ this._lastCommandMeta.nonPlay = true;
1657
1570
  this.log('info', `${cmdName} already claimed — waiting`);
1658
1571
  const timeMatch = result.match(/<t:(\d+):R>/);
1659
1572
  let waitSec;
@@ -1664,6 +1577,7 @@ class AccountWorker {
1664
1577
  const defaultWaits = { daily: 86400, weekly: 604800, monthly: 2592000 };
1665
1578
  waitSec = defaultWaits[cmdName] || 86400;
1666
1579
  }
1580
+ this._lastCommandMeta.nextRetrySec = waitSec;
1667
1581
  await this.setCooldown(cmdName, waitSec);
1668
1582
  this.doneToday.set(cmdName, Date.now() + waitSec * 1000);
1669
1583
  if (redis) try { await redis.set(`dkg:done:${this.account.id}:${cmdName}`, '1', 'EX', waitSec); } catch {}
@@ -1689,6 +1603,7 @@ class AccountWorker {
1689
1603
  // Track net earnings (add wins, subtract losses)
1690
1604
  this.stats.coins += (earned - spent);
1691
1605
  if (cmdResult.nextCooldownSec) {
1606
+ this._lastCommandMeta.nextRetrySec = Math.max(this._lastCommandMeta.nextRetrySec || 0, cmdResult.nextCooldownSec);
1692
1607
  await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
1693
1608
  this._lastCooldownOverride = cmdResult.nextCooldownSec;
1694
1609
  // Learn: record this cooldown as the known value for future fallback use
@@ -1732,6 +1647,7 @@ class AccountWorker {
1732
1647
  this._lastCommandMeta.nonPlay = true;
1733
1648
  this._lastCommandMeta.holdTightReason = reason;
1734
1649
  const holdSec = 35;
1650
+ this._lastCommandMeta.nextRetrySec = Math.max(this._lastCommandMeta.nextRetrySec || 0, holdSec);
1735
1651
  this.log('warn', `Hold Tight: /${reason} — ${holdSec}s global cooldown`);
1736
1652
  const reasonMap = { postmemes: 'pm', highlow: 'hl', blackjack: 'bj', 'work shift': 'work shift' };
1737
1653
  const mappedCmd = reasonMap[reason] || reason;
@@ -1746,10 +1662,10 @@ class AccountWorker {
1746
1662
  this._lastCommandMeta.nonPlay = true;
1747
1663
  this._lastCommandMeta.levelLocked = targetLv;
1748
1664
  this.log('warn', `Command /${cmdName} is LOCKED at Level ${targetLv} — starting quest unlock flow`);
1749
- if (this._startLevelQuests(targetLv)) {
1665
+ if (this._startLevelQuests(targetLv, cmdResult.levelQuestProgress || null)) {
1750
1666
  this.log('info', `[QUEST] Quest mode activated for Level ${targetLv} — grinding will resume after quests complete`);
1751
1667
  } else {
1752
- this.log('warn', `[QUEST] Level ${targetLv} quests already done or not defined — grinding continues (may re-trigger)`);
1668
+ this.log('warn', `[QUEST] Level ${targetLv} quests already done or not defined — waiting for verification`);
1753
1669
  }
1754
1670
  // Don't count as success — skip normal post-command processing
1755
1671
  return;
@@ -1911,10 +1827,21 @@ class AccountWorker {
1911
1827
  { cmd: 'search', times: 2 },
1912
1828
  { cmd: 'tidy', times: 2 },
1913
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
+ ],
1914
1836
  3: [
1915
- { cmd: 'work apply', times: 1 },
1916
- { cmd: 'work shift', times: 1 },
1917
- { 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' },
1918
1845
  ],
1919
1846
  5: [
1920
1847
  // "Lose 50,000 coins in /slots" etc — loseTarget means keep playing until cumulative losses hit 50k
@@ -1922,13 +1849,141 @@ class AccountWorker {
1922
1849
  { cmd: 'cointoss', loseTarget: 50000, bet: 10000, times: 50 },
1923
1850
  { cmd: 'snakeeyes', loseTarget: 50000, bet: 10000, times: 50 },
1924
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
+ ],
1925
1864
  };
1926
1865
 
1927
- _startLevelQuests(targetLevel) {
1866
+ _parseLevelQuestProgress(targetLevel, rawText) {
1867
+ const text = String(rawText || '');
1868
+ const out = {};
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
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
+
1930
+ return Object.keys(out).length > 0 ? out : null;
1931
+ }
1932
+
1933
+ _mergeLevelQuestProgress(targetLevel, progressHint) {
1934
+ if (!progressHint) return;
1935
+ const prev = this._levelQuestProgressCache.get(targetLevel) || {};
1936
+ const merged = { ...prev };
1937
+ for (const [cmd, val] of Object.entries(progressHint)) {
1938
+ const n = Number(val);
1939
+ if (!Number.isFinite(n) || n < 0) continue;
1940
+ merged[cmd] = Math.max(merged[cmd] || 0, n);
1941
+ }
1942
+ this._levelQuestProgressCache.set(targetLevel, merged);
1943
+ }
1944
+
1945
+ _startLevelQuests(targetLevel, progressHint = null) {
1928
1946
  if (this._levelQuestDone.has(targetLevel)) return false;
1929
1947
  const quests = AccountWorker.LEVEL_QUESTS[targetLevel];
1930
1948
  if (!quests || quests.length === 0) return false;
1931
- this._levelQuestQueue = quests.map(q => ({ ...q, level: targetLevel, _lostSoFar: 0 }));
1949
+
1950
+ this._mergeLevelQuestProgress(targetLevel, progressHint);
1951
+ const cached = this._levelQuestProgressCache.get(targetLevel) || {};
1952
+ const seededAll = quests
1953
+ .map(q => {
1954
+ const target = Number(q.loseTarget || 0);
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);
1977
+
1978
+ if (seededQueue.length === 0) {
1979
+ this.log('info', `[QUEST] Level ${targetLevel} tasks already complete from progress cache — verifying unlock`);
1980
+ this._verifyLevelUnlock = targetLevel;
1981
+ this.setStatus('verifying level...');
1982
+ this.commandQueue = null;
1983
+ return false;
1984
+ }
1985
+
1986
+ this._levelQuestQueue = seededQueue;
1932
1987
  this._levelQuestActive = true;
1933
1988
  this._questBetOverride = null;
1934
1989
  // Clear commandQueue so no stale grinding commands fire after quests finish
@@ -1936,13 +1991,81 @@ class AccountWorker {
1936
1991
  this._commandRunning = false; // cancel any in-flight grinding command
1937
1992
  this.log('info', `[QUEST] Level ${targetLevel} quests started — grinding PAUSED`);
1938
1993
  const questList = this._levelQuestQueue.map(q => {
1939
- if (q.loseTarget) return `"${q.cmd}" lose ⏣${q.loseTarget.toLocaleString()}`;
1994
+ if (q.loseTarget) {
1995
+ const rem = Math.max(0, q.loseTarget - (q._lostSoFar || 0));
1996
+ return `"${q.cmd}" lose ⏣${rem.toLocaleString()} more`;
1997
+ }
1940
1998
  return `"${q.cmd}" x${q.times}`;
1941
1999
  }).join(', ');
1942
2000
  this.log('info', `[QUEST] Quests: ${questList}`);
1943
2001
  return true;
1944
2002
  }
1945
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
+
1946
2069
  async buildCommandQueue() {
1947
2070
  const heap = new MinHeap();
1948
2071
  const now = Date.now();
@@ -2204,11 +2327,18 @@ class AccountWorker {
2204
2327
  if (currentLv >= targetLv) {
2205
2328
  this.log('success', `[QUEST] Level ${targetLv} verified ✓ — level unlocked!`);
2206
2329
  this._level = currentLv;
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
+ }
2207
2336
  } else {
2208
2337
  this.log('warn', `[QUEST] Level ${targetLv} NOT verified — still at ${currentLv}. Re-triggering quests.`);
2209
2338
  // Re-trigger quests for this level
2210
2339
  this._levelQuestDone.delete(targetLv);
2211
- this._startLevelQuests(targetLv);
2340
+ const cached = this._levelQuestProgressCache.get(targetLv) || null;
2341
+ this._startLevelQuests(targetLv, cached);
2212
2342
  this.tickTimeout = setTimeout(() => this.tick(), 2000);
2213
2343
  return;
2214
2344
  }
@@ -2223,7 +2353,22 @@ class AccountWorker {
2223
2353
 
2224
2354
  // BLOCK: quest mode active — run quests only, no normal grinding
2225
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
+
2226
2370
  const quest = this._levelQuestQueue[0];
2371
+ let nextQuestDelayMs = 1200;
2227
2372
  if (quest.loseTarget) {
2228
2373
  const remaining = Math.max(0, quest.loseTarget - (quest._lostSoFar || 0));
2229
2374
  const floorBet = quest._minBet || quest.bet || 1;
@@ -2231,7 +2376,6 @@ class AccountWorker {
2231
2376
  } else {
2232
2377
  this._questBetOverride = quest.bet || null;
2233
2378
  }
2234
- const questBetUsed = this._questBetOverride;
2235
2379
  const prefix = this.account.use_slash ? '/' : 'pls';
2236
2380
 
2237
2381
  if (quest.loseTarget) {
@@ -2250,7 +2394,13 @@ class AccountWorker {
2250
2394
  this._commandRunning = true;
2251
2395
  this.busy = true;
2252
2396
  await this.runCommand(quest.cmd, prefix);
2253
- const questMeta = this._lastCommandMeta || {};
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
+ }
2254
2404
  this._commandRunning = false;
2255
2405
  this.busy = false;
2256
2406
  this._questBetOverride = null;
@@ -2270,13 +2420,13 @@ class AccountWorker {
2270
2420
  quest._minBet = Math.max(quest._minBet || 1, questMeta.newMinBet);
2271
2421
  }
2272
2422
 
2273
- // Fallback only for actual played rounds where parsed/observed loss was zero.
2274
- if (!nonPlay && lostThisRound === 0 && (questBetUsed || quest.bet)) {
2275
- lostThisRound = questBetUsed || quest.bet;
2276
- }
2277
-
2278
2423
  if (!nonPlay) {
2279
2424
  quest._lostSoFar = (quest._lostSoFar || 0) + lostThisRound;
2425
+ this._mergeLevelQuestProgress(quest.level, { [quest.cmd]: quest._lostSoFar });
2426
+ if (qEntry) qEntry.success++;
2427
+ } else if (qEntry) {
2428
+ qEntry.nonPlay++;
2429
+ qEntry.failed++;
2280
2430
  }
2281
2431
 
2282
2432
  this.log('info', `[QUEST] ${quest.cmd} — lost ⏣${quest._lostSoFar.toLocaleString()} / ⏣${quest.loseTarget.toLocaleString()}`);
@@ -2295,10 +2445,57 @@ class AccountWorker {
2295
2445
  questStepDone = true;
2296
2446
  this.log('info', `[QUEST] ${quest.cmd} max attempts reached — moving on`);
2297
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
+ }
2298
2455
  } else {
2299
2456
  // Normal times-based quest
2300
- 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
+ }
2301
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)));
2302
2499
  }
2303
2500
 
2304
2501
  if (questStepDone) this._levelQuestQueue.shift();
@@ -2307,6 +2504,11 @@ class AccountWorker {
2307
2504
  this._levelQuestActive = false;
2308
2505
  this._levelQuestDone.add(quest.level);
2309
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
+ }
2310
2512
  this.log('success', `[QUEST] Level ${justCompletedLevel} quests DONE — verifying unlock...`);
2311
2513
  // Null out commandQueue so buildCommandQueue picks only unlocked commands
2312
2514
  this.commandQueue = null;
@@ -2316,7 +2518,7 @@ class AccountWorker {
2316
2518
  this.tickTimeout = setTimeout(() => this.tick(), 3000);
2317
2519
  return;
2318
2520
  }
2319
- this.tickTimeout = setTimeout(() => this.tick(), 2500);
2521
+ this.tickTimeout = setTimeout(() => this.tick(), nextQuestDelayMs);
2320
2522
  return;
2321
2523
  }
2322
2524
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.105.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"