dankgrinder 8.107.0 → 8.110.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.
- package/lib/commands/adventure.js +27 -9
- package/lib/commands/index.js +2 -1
- package/lib/grinder.js +395 -164
- package/lib/rawLogger.js +23 -0
- package/package.json +1 -1
|
@@ -108,14 +108,31 @@ const SAFE_KEYWORDS = Object.freeze(['flee', 'run', 'hide', 'avoid', 'ignore', '
|
|
|
108
108
|
const RISKY_KEYWORDS = Object.freeze(['reach', 'grab', 'fight', 'attack', 'steal', 'open', 'touch', 'eat', 'drink']);
|
|
109
109
|
const ADVENTURE_PREFERRED_TYPES = Object.freeze(['space', 'out west']);
|
|
110
110
|
|
|
111
|
-
function pickSafeChoice(choices) {
|
|
111
|
+
function pickSafeChoice(choices, adventureAnswers) {
|
|
112
112
|
if (choices.length === 0) return null;
|
|
113
113
|
if (choices.length === 1) return choices[0];
|
|
114
114
|
|
|
115
|
-
// Check labels for safe/risky keywords
|
|
116
115
|
const labels = choices.map(b => (b.label || '').toLowerCase());
|
|
117
116
|
|
|
118
|
-
//
|
|
117
|
+
// ── 1) Config answer maps (dmg pattern — highest priority) ──
|
|
118
|
+
if (adventureAnswers) {
|
|
119
|
+
for (const [question, answers] of Object.entries(adventureAnswers)) {
|
|
120
|
+
const matched = labels.find(l => l.includes(question));
|
|
121
|
+
if (matched && Array.isArray(answers)) {
|
|
122
|
+
for (const answer of answers) {
|
|
123
|
+
const found = choices.find(c =>
|
|
124
|
+
(c.label || '').toLowerCase().includes(answer.toLowerCase())
|
|
125
|
+
);
|
|
126
|
+
if (found) {
|
|
127
|
+
LOG.info(`[adventure] Config answer: "${found.label}" matched question "${question}"`);
|
|
128
|
+
return found;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── 2) Hardcoded safe keywords ──
|
|
119
136
|
for (let i = 0; i < labels.length; i++) {
|
|
120
137
|
for (let k = 0; k < SAFE_KEYWORDS.length; k++) {
|
|
121
138
|
const kw = SAFE_KEYWORDS[k];
|
|
@@ -126,7 +143,7 @@ function pickSafeChoice(choices) {
|
|
|
126
143
|
}
|
|
127
144
|
}
|
|
128
145
|
|
|
129
|
-
// Avoid risky keywords
|
|
146
|
+
// ── 3) Avoid risky keywords ──
|
|
130
147
|
const nonRisky = choices.filter((b, i) => {
|
|
131
148
|
return !RISKY_KEYWORDS.some(kw => labels[i].includes(kw));
|
|
132
149
|
});
|
|
@@ -143,7 +160,7 @@ function pickSafeChoice(choices) {
|
|
|
143
160
|
}
|
|
144
161
|
|
|
145
162
|
// ── Play through all adventure rounds ────────────────────────────
|
|
146
|
-
async function playAdventureRounds(channel, msg) {
|
|
163
|
+
async function playAdventureRounds(channel, msg, adventureAnswers) {
|
|
147
164
|
let current = msg;
|
|
148
165
|
let interactions = 0;
|
|
149
166
|
const MAX_INTERACTIONS = 30;
|
|
@@ -171,7 +188,7 @@ async function playAdventureRounds(channel, msg) {
|
|
|
171
188
|
// ── If there are choices, pick one first ─────────────────
|
|
172
189
|
if (choices.length > 0) {
|
|
173
190
|
LOG.info(`[adventure] ${choices.length} choices: [${choices.map(b => `"${b.label}"`).join(', ')}]`);
|
|
174
|
-
const choice = pickSafeChoice(choices);
|
|
191
|
+
const choice = pickSafeChoice(choices, adventureAnswers);
|
|
175
192
|
if (choice) {
|
|
176
193
|
LOG.info(`[adventure] → Choosing: "${choice.label}"`);
|
|
177
194
|
await sleep(50);
|
|
@@ -258,9 +275,10 @@ async function playAdventureRounds(channel, msg) {
|
|
|
258
275
|
* @param {object} opts.channel - Discord channel
|
|
259
276
|
* @param {function} opts.waitForDankMemer - Waits for Dank Memer response
|
|
260
277
|
* @param {object} [opts.client] - Discord client (for modal handling in shop)
|
|
278
|
+
* @param {object} [opts.adventureAnswers] - Per-type answer map from AccountWorker.ADVENTURE_ANSWERS
|
|
261
279
|
* @returns {Promise<{result: string, coins: number, nextCooldownSec: number|null}>}
|
|
262
280
|
*/
|
|
263
|
-
async function runAdventure({ channel, waitForDankMemer, client }) {
|
|
281
|
+
async function runAdventure({ channel, waitForDankMemer, client, adventureAnswers }) {
|
|
264
282
|
LOG.cmd(`${c.white}${c.bold}pls adventure${c.reset}`);
|
|
265
283
|
|
|
266
284
|
// Step 1: Send the command
|
|
@@ -334,7 +352,7 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
|
|
|
334
352
|
if (hasNextBtn && menus.length === 0) {
|
|
335
353
|
// Already mid-adventure from a previous run — jump straight to rounds
|
|
336
354
|
LOG.info('[adventure] Resuming mid-adventure...');
|
|
337
|
-
const { text: finalText, coins, interactions, rewards, finalMsg } = await playAdventureRounds(channel, response);
|
|
355
|
+
const { text: finalText, coins, interactions, rewards, finalMsg } = await playAdventureRounds(channel, response, adventureAnswers);
|
|
338
356
|
return buildResult(finalText, coins, interactions, rewards, finalMsg);
|
|
339
357
|
}
|
|
340
358
|
|
|
@@ -434,7 +452,7 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
|
|
|
434
452
|
}
|
|
435
453
|
|
|
436
454
|
// ── Play through all adventure rounds ──────────────────────
|
|
437
|
-
const { text: finalText, coins, interactions, rewards, finalMsg } = await playAdventureRounds(channel, response);
|
|
455
|
+
const { text: finalText, coins, interactions, rewards, finalMsg } = await playAdventureRounds(channel, response, adventureAnswers);
|
|
438
456
|
return buildResult(finalText, coins, interactions, rewards, finalMsg);
|
|
439
457
|
}
|
|
440
458
|
|
package/lib/commands/index.js
CHANGED
|
@@ -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
|
@@ -598,6 +598,37 @@ class MinHeap {
|
|
|
598
598
|
|
|
599
599
|
get size() { return this.heap.length; }
|
|
600
600
|
|
|
601
|
+
// ── Message handler registration (dmg pattern) ──────────────
|
|
602
|
+
// Allows plugins/commands to register handlers for gateway events.
|
|
603
|
+
// handlers: { messageCreate: Map<name, fn>, messageUpdate: Map<name, fn> }
|
|
604
|
+
// ── Dispatch events to registered handlers (dmg pattern) ──
|
|
605
|
+
_dispatchToHandlers(event, msg) {
|
|
606
|
+
const handlers = this._handlers[event];
|
|
607
|
+
if (!handlers || handlers.size === 0) return;
|
|
608
|
+
for (const [name, fn] of handlers) {
|
|
609
|
+
try {
|
|
610
|
+
const result = fn(msg);
|
|
611
|
+
if (result && typeof result.then === 'function') {
|
|
612
|
+
result.catch(e => this.log('error', `[handler:${name}] ${e.message}`));
|
|
613
|
+
}
|
|
614
|
+
} catch (e) {
|
|
615
|
+
this.log('error', `[handler:${name}] ${e.message}`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
_registerHandler(event, name, fn) {
|
|
621
|
+
if (this._handlers[event]) {
|
|
622
|
+
this._handlers[event].set(name, fn);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
_unregisterHandler(event, name) {
|
|
627
|
+
if (this._handlers[event]) {
|
|
628
|
+
this._handlers[event].delete(name);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
601
632
|
_bubbleUp(i) {
|
|
602
633
|
while (i > 0) {
|
|
603
634
|
const parent = (i - 1) >> 1;
|
|
@@ -670,12 +701,21 @@ class AccountWorker {
|
|
|
670
701
|
this._levelQuestActive = false;
|
|
671
702
|
this._levelQuestQueue = [];
|
|
672
703
|
this._levelQuestDone = new Set();
|
|
673
|
-
|
|
704
|
+
this._levelQuestProgressCache = new Map();
|
|
705
|
+
this._levelQuestRunMap = new Map(); // level -> { startedAt, updatedAt, entries: Map<cmd, state> }
|
|
674
706
|
this._questBetOverride = null;
|
|
675
|
-
|
|
707
|
+
this._lastCommandMeta = null;
|
|
676
708
|
this._commandRunning = false; // prevents grinding commands from overlapping with quest commands
|
|
677
709
|
this._verifyLevelUnlock = null; // holds level to verify after quest completion
|
|
678
710
|
this.commandQueue = null;
|
|
711
|
+
|
|
712
|
+
// ── Message handler routing (dmg pattern) ─────────────────
|
|
713
|
+
// Map of event → handler functions. Plugins register here.
|
|
714
|
+
// Usage: _handlers.messageCreate.get('adventure')?.(msg)
|
|
715
|
+
this._handlers = {
|
|
716
|
+
messageCreate: new Map(),
|
|
717
|
+
messageUpdate: new Map(),
|
|
718
|
+
};
|
|
679
719
|
this.lastHealthCheck = Date.now();
|
|
680
720
|
this.doneToday = new Map();
|
|
681
721
|
|
|
@@ -761,6 +801,8 @@ class AccountWorker {
|
|
|
761
801
|
}
|
|
762
802
|
function handler(msg) {
|
|
763
803
|
if (msg.author.id === DANK_MEMER_ID && msg.channel.id === self.channel.id) {
|
|
804
|
+
// ── Dispatch to registered handlers (dmg pattern) ────
|
|
805
|
+
self._dispatchToHandlers('messageCreate', msg);
|
|
764
806
|
const hasComponentPayload = Array.isArray(msg.components)
|
|
765
807
|
&& msg.components.some(c => c && (c.components || c.content || c.type || c.label || c.customId));
|
|
766
808
|
const hasContent = (msg.content && msg.content.length > 0)
|
|
@@ -790,6 +832,8 @@ class AccountWorker {
|
|
|
790
832
|
// Reject edits to messages created well before our command was sent
|
|
791
833
|
const msgTs = newMsg.createdTimestamp || newMsg.createdAt?.getTime?.() || 0;
|
|
792
834
|
if (msgTs > 0 && msgTs < sentAt - 1500) return;
|
|
835
|
+
// ── Dispatch to registered handlers ─────────────────
|
|
836
|
+
self._dispatchToHandlers('messageUpdate', newMsg);
|
|
793
837
|
cleanup();
|
|
794
838
|
resolve(newMsg);
|
|
795
839
|
}
|
|
@@ -843,148 +887,13 @@ class AccountWorker {
|
|
|
843
887
|
}
|
|
844
888
|
|
|
845
889
|
async buyItem(itemName, quantity = 1) {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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;
|
|
890
|
+
return !!(await commands.buyItem({
|
|
891
|
+
channel: this.channel,
|
|
892
|
+
waitForDankMemer: (timeout) => this.waitForDankMemer(timeout),
|
|
893
|
+
client: this.client,
|
|
894
|
+
itemName,
|
|
895
|
+
quantity,
|
|
896
|
+
}));
|
|
988
897
|
}
|
|
989
898
|
|
|
990
899
|
// ── Check Balance ───────────────────────────────────────────
|
|
@@ -1387,7 +1296,7 @@ class AccountWorker {
|
|
|
1387
1296
|
// Each modular command handler sends the command, waits for response,
|
|
1388
1297
|
// handles Hold Tight / cooldowns / item-buying internally.
|
|
1389
1298
|
async runCommand(cmdName, prefix) {
|
|
1390
|
-
this._lastCommandMeta = { cmdName, nonPlay: false, spent: 0, earned: 0, holdTightReason: null, newMinBet: null, levelLocked: null };
|
|
1299
|
+
this._lastCommandMeta = { cmdName, nonPlay: false, spent: 0, earned: 0, holdTightReason: null, newMinBet: null, levelLocked: null, nextRetrySec: 0 };
|
|
1391
1300
|
let cmdString;
|
|
1392
1301
|
const bjBet = Math.max(5000, this.account.bet_amount || 5000);
|
|
1393
1302
|
const gambBet = Math.max(10000, this.account.bet_amount || 10000);
|
|
@@ -1490,6 +1399,7 @@ class AccountWorker {
|
|
|
1490
1399
|
client: this.client,
|
|
1491
1400
|
safeAnswers: cmdName === 'search' ? safeParseJSON(this.account.search_answers, []) :
|
|
1492
1401
|
cmdName === 'crime' ? safeParseJSON(this.account.crime_answers, []) : [],
|
|
1402
|
+
adventureAnswers: AccountWorker.ADVENTURE_ANSWERS,
|
|
1493
1403
|
betAmount: this._questBetOverride || (['blackjack'].includes(cmdName) ? bjBet : gambBet),
|
|
1494
1404
|
accountId: this.account.id,
|
|
1495
1405
|
redis,
|
|
@@ -1502,7 +1412,7 @@ class AccountWorker {
|
|
|
1502
1412
|
case 'search': cmdResult = await commands.runSearch(cmdOpts); break;
|
|
1503
1413
|
case 'crime': cmdResult = await commands.runCrime(cmdOpts); break;
|
|
1504
1414
|
case 'hl': cmdResult = await commands.runHighLow(cmdOpts); break;
|
|
1505
|
-
|
|
1415
|
+
case 'farm': cmdResult = await commands.runFarm(cmdOpts); break;
|
|
1506
1416
|
case 'pm': cmdResult = await commands.runPostMemes(cmdOpts); break;
|
|
1507
1417
|
case 'hunt': cmdResult = await commands.runHunt(cmdOpts); break;
|
|
1508
1418
|
case 'dig': cmdResult = await commands.runDig(cmdOpts); break;
|
|
@@ -1512,6 +1422,20 @@ class AccountWorker {
|
|
|
1512
1422
|
case 'blackjack': cmdResult = await commands.runBlackjack(cmdOpts); break;
|
|
1513
1423
|
case 'trivia': cmdResult = await commands.runTrivia(cmdOpts); break;
|
|
1514
1424
|
case 'work shift': cmdResult = await commands.runWorkShift(cmdOpts); break;
|
|
1425
|
+
case 'work apply': {
|
|
1426
|
+
const ar = await commands.autoApplyForJob(cmdOpts);
|
|
1427
|
+
cmdResult = ar?.applied
|
|
1428
|
+
? { result: 'work apply completed', coins: 0, nextCooldownSec: 8 }
|
|
1429
|
+
: { result: `work apply failed${ar?.cooldownSec ? ` (${Math.ceil(ar.cooldownSec / 60)}m)` : ''}`, coins: 0, nonPlay: true, nextCooldownSec: ar?.cooldownSec || 60 };
|
|
1430
|
+
break;
|
|
1431
|
+
}
|
|
1432
|
+
case 'shop view': {
|
|
1433
|
+
const bought = await commands.buyItem({ ...cmdOpts, itemName: 'Shovel', quantity: 1 });
|
|
1434
|
+
cmdResult = bought
|
|
1435
|
+
? { result: 'shop buy item completed', coins: 0, nextCooldownSec: 10 }
|
|
1436
|
+
: { result: 'shop buy item failed', coins: 0, nonPlay: true, nextCooldownSec: 25 };
|
|
1437
|
+
break;
|
|
1438
|
+
}
|
|
1515
1439
|
case 'cointoss': cmdResult = await commands.runCointoss(cmdOpts); break;
|
|
1516
1440
|
case 'roulette': cmdResult = await commands.runRoulette(cmdOpts); break;
|
|
1517
1441
|
case 'slots': cmdResult = await commands.runSlots(cmdOpts); break;
|
|
@@ -1526,6 +1450,7 @@ class AccountWorker {
|
|
|
1526
1450
|
|
|
1527
1451
|
const result = cmdResult.result || 'done';
|
|
1528
1452
|
const resultLower = result.toLowerCase();
|
|
1453
|
+
this._lastCommandMeta.result = result;
|
|
1529
1454
|
|
|
1530
1455
|
// Rate limit detection — progressive backoff based on frequency
|
|
1531
1456
|
if (resultLower.includes('slow down') || resultLower.includes('rate limit') || resultLower.includes('too fast')) {
|
|
@@ -1535,6 +1460,7 @@ class AccountWorker {
|
|
|
1535
1460
|
const cooldownSec = Math.min(30 * Math.pow(2, Math.min(this._rateLimitHits - 1, 3)), 300);
|
|
1536
1461
|
this.log('warn', `Rate limited! ${cooldownSec}s cooldown (hit #${this._rateLimitHits})`);
|
|
1537
1462
|
this.globalCooldownUntil = Date.now() + cooldownSec * 1000;
|
|
1463
|
+
this._lastCommandMeta.nextRetrySec = cooldownSec;
|
|
1538
1464
|
await this.setCooldown(cmdName, cooldownSec);
|
|
1539
1465
|
// Reset rate limit count after 10 minutes of no hits
|
|
1540
1466
|
setTimeout(() => { if (this._rateLimitHits > 0) this._rateLimitHits = Math.max(0, this._rateLimitHits - 1); }, 600_000);
|
|
@@ -1545,6 +1471,8 @@ class AccountWorker {
|
|
|
1545
1471
|
if (resultLower.includes('cannot post another meme') || resultLower.includes('dead meme')) {
|
|
1546
1472
|
const minMatch = result.match(/(\d+)\s*minute/i);
|
|
1547
1473
|
const cdSec = minMatch ? parseInt(minMatch[1]) * 60 + 30 : 150; // dead meme = N min + 30s buffer
|
|
1474
|
+
this._lastCommandMeta.nonPlay = true;
|
|
1475
|
+
this._lastCommandMeta.nextRetrySec = cdSec;
|
|
1548
1476
|
this.log('warn', `${cmdName} on cooldown: ${cdSec}s`);
|
|
1549
1477
|
await this.setCooldown(cmdName, cdSec);
|
|
1550
1478
|
return;
|
|
@@ -1565,6 +1493,28 @@ class AccountWorker {
|
|
|
1565
1493
|
this._mergeLevelQuestProgress(targetLv, progress);
|
|
1566
1494
|
}
|
|
1567
1495
|
this.log('warn', `Command /${cmdName} requires Level ${targetLv} quests`);
|
|
1496
|
+
} else {
|
|
1497
|
+
// Fallback: some responses are truncated/ephemeral and omit "Level X" in result text.
|
|
1498
|
+
// Run one profile check and infer lock level from raw profile text.
|
|
1499
|
+
try {
|
|
1500
|
+
const profile = await this.checkProfile(true);
|
|
1501
|
+
const profileText = String(profile?.rawText || '');
|
|
1502
|
+
if (profileText.toLowerCase().includes('not unlocked') || profileText.toLowerCase().includes('have not unlocked')) {
|
|
1503
|
+
const pm = profileText.match(/level\s*(\d+)/i);
|
|
1504
|
+
if (pm) {
|
|
1505
|
+
const targetLv = parseInt(pm[1], 10);
|
|
1506
|
+
if (Number.isFinite(targetLv) && targetLv > 0) {
|
|
1507
|
+
cmdResult.levelLocked = targetLv;
|
|
1508
|
+
const progress = this._parseLevelQuestProgress(targetLv, profileText);
|
|
1509
|
+
if (progress) {
|
|
1510
|
+
cmdResult.levelQuestProgress = progress;
|
|
1511
|
+
this._mergeLevelQuestProgress(targetLv, progress);
|
|
1512
|
+
}
|
|
1513
|
+
this.log('warn', `Command /${cmdName} requires Level ${targetLv} quests (profile fallback)`);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
} catch {}
|
|
1568
1518
|
}
|
|
1569
1519
|
}
|
|
1570
1520
|
|
|
@@ -1660,6 +1610,7 @@ class AccountWorker {
|
|
|
1660
1610
|
if (resultLower.includes('already got your daily') || resultLower.includes('already got your weekly') ||
|
|
1661
1611
|
resultLower.includes('already got your monthly') || resultLower.includes('already claimed') ||
|
|
1662
1612
|
resultLower.includes('try again <t:')) {
|
|
1613
|
+
this._lastCommandMeta.nonPlay = true;
|
|
1663
1614
|
this.log('info', `${cmdName} already claimed — waiting`);
|
|
1664
1615
|
const timeMatch = result.match(/<t:(\d+):R>/);
|
|
1665
1616
|
let waitSec;
|
|
@@ -1670,6 +1621,7 @@ class AccountWorker {
|
|
|
1670
1621
|
const defaultWaits = { daily: 86400, weekly: 604800, monthly: 2592000 };
|
|
1671
1622
|
waitSec = defaultWaits[cmdName] || 86400;
|
|
1672
1623
|
}
|
|
1624
|
+
this._lastCommandMeta.nextRetrySec = waitSec;
|
|
1673
1625
|
await this.setCooldown(cmdName, waitSec);
|
|
1674
1626
|
this.doneToday.set(cmdName, Date.now() + waitSec * 1000);
|
|
1675
1627
|
if (redis) try { await redis.set(`dkg:done:${this.account.id}:${cmdName}`, '1', 'EX', waitSec); } catch {}
|
|
@@ -1695,6 +1647,7 @@ class AccountWorker {
|
|
|
1695
1647
|
// Track net earnings (add wins, subtract losses)
|
|
1696
1648
|
this.stats.coins += (earned - spent);
|
|
1697
1649
|
if (cmdResult.nextCooldownSec) {
|
|
1650
|
+
this._lastCommandMeta.nextRetrySec = Math.max(this._lastCommandMeta.nextRetrySec || 0, cmdResult.nextCooldownSec);
|
|
1698
1651
|
await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
|
|
1699
1652
|
this._lastCooldownOverride = cmdResult.nextCooldownSec;
|
|
1700
1653
|
// Learn: record this cooldown as the known value for future fallback use
|
|
@@ -1738,6 +1691,7 @@ class AccountWorker {
|
|
|
1738
1691
|
this._lastCommandMeta.nonPlay = true;
|
|
1739
1692
|
this._lastCommandMeta.holdTightReason = reason;
|
|
1740
1693
|
const holdSec = 35;
|
|
1694
|
+
this._lastCommandMeta.nextRetrySec = Math.max(this._lastCommandMeta.nextRetrySec || 0, holdSec);
|
|
1741
1695
|
this.log('warn', `Hold Tight: /${reason} — ${holdSec}s global cooldown`);
|
|
1742
1696
|
const reasonMap = { postmemes: 'pm', highlow: 'hl', blackjack: 'bj', 'work shift': 'work shift' };
|
|
1743
1697
|
const mappedCmd = reasonMap[reason] || reason;
|
|
@@ -1911,16 +1865,63 @@ class AccountWorker {
|
|
|
1911
1865
|
].map(Object.freeze);
|
|
1912
1866
|
|
|
1913
1867
|
// ── Level Quest System — blocks grinding until quests complete ──
|
|
1868
|
+
// ── Adventure answer maps (per type) ──────────────────────────
|
|
1869
|
+
// Pattern: question text → answer choice to click
|
|
1870
|
+
// Supports: space, out west, brazil, vacation, halloween, museum
|
|
1871
|
+
static ADVENTURE_ANSWERS = {
|
|
1872
|
+
space: {
|
|
1873
|
+
'what do you want to do': ['search for debris', 'scavenge wreckage'],
|
|
1874
|
+
'how do you respond': ['stay calm', 'analyze situation'],
|
|
1875
|
+
'what do you choose': ['try to communicate', 'observe first'],
|
|
1876
|
+
},
|
|
1877
|
+
west: {
|
|
1878
|
+
'what do you do': ['draw first', 'hide behind cover'],
|
|
1879
|
+
'how do you respond': ['draw', 'shoot'],
|
|
1880
|
+
'sheriff wants': ['accept bounty', 'decline'],
|
|
1881
|
+
},
|
|
1882
|
+
brazil: {
|
|
1883
|
+
'what do you want to do': ['search for treasure', 'ask locals'],
|
|
1884
|
+
'how do you respond': ['negotiate', 'accept offer'],
|
|
1885
|
+
'locals offer': ['take the deal', 'refuse'],
|
|
1886
|
+
},
|
|
1887
|
+
vacation: {
|
|
1888
|
+
'stranger offers': ['accept', 'decline'],
|
|
1889
|
+
'what do you do': ['explore', 'rest'],
|
|
1890
|
+
'something suspicious': ['investigate', 'ignore'],
|
|
1891
|
+
},
|
|
1892
|
+
halloween: {
|
|
1893
|
+
'knock at door': ['open it', 'hide'],
|
|
1894
|
+
'what do you do': ['investigate', 'run away'],
|
|
1895
|
+
'ghost appears': ['speak to it', 'hide'],
|
|
1896
|
+
},
|
|
1897
|
+
museum: {
|
|
1898
|
+
'what do you do': ['touch exhibit', 'keep distance'],
|
|
1899
|
+
'alarm sounds': ['hide', 'run'],
|
|
1900
|
+
'guard asks': ['tell truth', 'lie'],
|
|
1901
|
+
},
|
|
1902
|
+
};
|
|
1903
|
+
|
|
1914
1904
|
static LEVEL_QUESTS = {
|
|
1915
1905
|
1: [
|
|
1916
1906
|
{ cmd: 'beg', times: 2 },
|
|
1917
1907
|
{ cmd: 'search', times: 2 },
|
|
1918
1908
|
{ cmd: 'tidy', times: 2 },
|
|
1919
1909
|
],
|
|
1910
|
+
2: [
|
|
1911
|
+
{ cmd: 'inventory', times: 1, progressMatch: 'inventory', progressSlash: 'inventory' },
|
|
1912
|
+
{ cmd: 'balance', times: 1, progressMatch: 'balance', progressSlash: 'balance' },
|
|
1913
|
+
{ cmd: 'hunt', times: 2, progressMatch: 'hunt', progressSlash: 'hunt' },
|
|
1914
|
+
{ cmd: 'dig', times: 2, progressMatch: 'dig', progressSlash: 'dig' },
|
|
1915
|
+
],
|
|
1920
1916
|
3: [
|
|
1921
|
-
{ cmd: 'work apply',
|
|
1922
|
-
{ cmd: 'work shift',
|
|
1923
|
-
{ cmd: 'shop sell
|
|
1917
|
+
{ cmd: 'work apply', times: 1, progressMatch: 'get a job', progressSlash: 'work apply' },
|
|
1918
|
+
{ cmd: 'work shift', times: 1, progressMatch: 'work a shift', progressSlash: 'work shift' },
|
|
1919
|
+
{ cmd: 'shop sell item shovel 1',times: 2, progressMatch: 'sell items', progressSlash: 'shop sell' },
|
|
1920
|
+
{ cmd: 'shop view', times: 1, progressMatch: 'buy an item', progressSlash: 'shop view' },
|
|
1921
|
+
],
|
|
1922
|
+
4: [
|
|
1923
|
+
// Keep this command-driven so the quest runner can continue progression.
|
|
1924
|
+
{ cmd: 'bal', times: 1, progressMatch: 'balance', progressSlash: 'balance' },
|
|
1924
1925
|
],
|
|
1925
1926
|
5: [
|
|
1926
1927
|
// "Lose 50,000 coins in /slots" etc — loseTarget means keep playing until cumulative losses hit 50k
|
|
@@ -1928,24 +1929,89 @@ class AccountWorker {
|
|
|
1928
1929
|
{ cmd: 'cointoss', loseTarget: 50000, bet: 10000, times: 50 },
|
|
1929
1930
|
{ cmd: 'snakeeyes', loseTarget: 50000, bet: 10000, times: 50 },
|
|
1930
1931
|
],
|
|
1932
|
+
6: [
|
|
1933
|
+
// Slash text in lock-card maps to regular pls command strings below.
|
|
1934
|
+
{ cmd: 'item new player pack', times: 1, progressMatch: 'new player pack', progressSlash: 'item' },
|
|
1935
|
+
{ cmd: 'use player 1', times: 1, progressMatch: 'new player pack', progressSlash: 'use' },
|
|
1936
|
+
{ cmd: 'use normie 1', times: 1, progressMatch: 'normie box', progressSlash: 'use' },
|
|
1937
|
+
],
|
|
1938
|
+
7: [
|
|
1939
|
+
{ cmd: 'title set newbie', times: 1, progressMatch: 'newbie title', progressSlash: 'title' },
|
|
1940
|
+
{ cmd: 'profile', times: 1, progressMatch: 'profile', progressSlash: 'profile' },
|
|
1941
|
+
{ cmd: 'daily', times: 1, progressMatch: 'daily', progressSlash: 'daily' },
|
|
1942
|
+
{ cmd: 'hl', times: 3, progressMatch: 'highlow', progressSlash: 'highlow' },
|
|
1943
|
+
],
|
|
1931
1944
|
};
|
|
1932
1945
|
|
|
1933
1946
|
_parseLevelQuestProgress(targetLevel, rawText) {
|
|
1934
|
-
if (targetLevel !== 5) return null;
|
|
1935
1947
|
const text = String(rawText || '');
|
|
1936
1948
|
const out = {};
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1949
|
+
|
|
1950
|
+
if (targetLevel === 5) {
|
|
1951
|
+
const re = /(\d[\d,]*)\s*\/\s*50,000[\s\S]{0,180}?<\/(slots|cointoss|snakeeyes):/gi;
|
|
1952
|
+
let m;
|
|
1953
|
+
while ((m = re.exec(text)) !== null) {
|
|
1954
|
+
const cur = parseInt(String(m[1] || '').replace(/,/g, ''), 10);
|
|
1955
|
+
const cmd = String(m[2] || '').toLowerCase();
|
|
1956
|
+
if (Number.isFinite(cur) && cmd) out[cmd] = Math.max(0, cur);
|
|
1957
|
+
}
|
|
1943
1958
|
}
|
|
1959
|
+
|
|
1960
|
+
if (targetLevel === 6) {
|
|
1961
|
+
// handled below by generic times-based parser
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
if (targetLevel !== 5) {
|
|
1965
|
+
const quests = AccountWorker.LEVEL_QUESTS[targetLevel] || [];
|
|
1966
|
+
const lines = text.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
1967
|
+
for (const q of quests) {
|
|
1968
|
+
if (q.loseTarget) continue;
|
|
1969
|
+
const cmd = String(q.cmd || '').toLowerCase();
|
|
1970
|
+
const cmdParts = cmd.split(/\s+/).filter(Boolean);
|
|
1971
|
+
if (cmdParts.length === 0) continue;
|
|
1972
|
+
|
|
1973
|
+
const slashHints = new Set();
|
|
1974
|
+
slashHints.add(cmdParts[0]);
|
|
1975
|
+
if (q.progressSlash) {
|
|
1976
|
+
for (const p of String(q.progressSlash).toLowerCase().split(/[\s,/|]+/).filter(Boolean)) {
|
|
1977
|
+
slashHints.add(p);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
if (cmd === 'hl') slashHints.add('highlow');
|
|
1981
|
+
if (cmd === 'highlow') slashHints.add('hl');
|
|
1982
|
+
|
|
1983
|
+
const matchPhrase = String(q.progressMatch || q.cmd || '').toLowerCase();
|
|
1984
|
+
const tail = matchPhrase
|
|
1985
|
+
.split(/\s+/)
|
|
1986
|
+
.filter(Boolean)
|
|
1987
|
+
.filter(t => !/^\d+$/.test(t))
|
|
1988
|
+
.filter(t => t !== cmdParts[0]);
|
|
1989
|
+
|
|
1990
|
+
for (const line of lines) {
|
|
1991
|
+
const lower = line.toLowerCase();
|
|
1992
|
+
const hasSlash = Array.from(slashHints).some(h => lower.includes(`/${h}`));
|
|
1993
|
+
if (!hasSlash) continue;
|
|
1994
|
+
|
|
1995
|
+
let ok = true;
|
|
1996
|
+
for (const token of tail) {
|
|
1997
|
+
if (!lower.includes(token)) { ok = false; break; }
|
|
1998
|
+
}
|
|
1999
|
+
if (!ok) continue;
|
|
2000
|
+
|
|
2001
|
+
const m = lower.match(/(\d[\d,]*)\s*\/\s*(\d[\d,]*)/);
|
|
2002
|
+
if (!m) continue;
|
|
2003
|
+
const cur = parseInt(String(m[1] || '').replace(/,/g, ''), 10);
|
|
2004
|
+
if (!Number.isFinite(cur)) continue;
|
|
2005
|
+
out[cmd] = Math.max(out[cmd] || 0, cur);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
|
|
1944
2010
|
return Object.keys(out).length > 0 ? out : null;
|
|
1945
2011
|
}
|
|
1946
2012
|
|
|
1947
2013
|
_mergeLevelQuestProgress(targetLevel, progressHint) {
|
|
1948
|
-
if (!progressHint
|
|
2014
|
+
if (!progressHint) return;
|
|
1949
2015
|
const prev = this._levelQuestProgressCache.get(targetLevel) || {};
|
|
1950
2016
|
const merged = { ...prev };
|
|
1951
2017
|
for (const [cmd, val] of Object.entries(progressHint)) {
|
|
@@ -1963,13 +2029,31 @@ class AccountWorker {
|
|
|
1963
2029
|
|
|
1964
2030
|
this._mergeLevelQuestProgress(targetLevel, progressHint);
|
|
1965
2031
|
const cached = this._levelQuestProgressCache.get(targetLevel) || {};
|
|
1966
|
-
const
|
|
2032
|
+
const seededAll = quests
|
|
1967
2033
|
.map(q => {
|
|
1968
2034
|
const target = Number(q.loseTarget || 0);
|
|
1969
|
-
const
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2035
|
+
const seededLoss = target > 0 ? Math.min(target, Math.max(0, Number(cached[q.cmd] || 0))) : 0;
|
|
2036
|
+
const seededDone = target === 0 ? Math.max(0, Math.min(Number(q.times || 0), Number(cached[q.cmd] || 0))) : 0;
|
|
2037
|
+
const timesRemaining = target === 0 ? Math.max(0, Number(q.times || 0) - seededDone) : Number(q.times || 0);
|
|
2038
|
+
const defaultCdSec = this._getCommandDefaultCooldownSec(q.cmd);
|
|
2039
|
+
return {
|
|
2040
|
+
...q,
|
|
2041
|
+
level: targetLevel,
|
|
2042
|
+
_lostSoFar: seededLoss,
|
|
2043
|
+
_doneSoFar: seededDone,
|
|
2044
|
+
times: timesRemaining,
|
|
2045
|
+
_defaultCdSec: defaultCdSec,
|
|
2046
|
+
_nextRunAt: 0,
|
|
2047
|
+
};
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
const seededQueue = seededAll
|
|
2051
|
+
.filter(q => {
|
|
2052
|
+
if (q.loseTarget) return q._lostSoFar < q.loseTarget;
|
|
2053
|
+
return q.times > 0;
|
|
2054
|
+
});
|
|
2055
|
+
|
|
2056
|
+
this._initLevelQuestRunState(targetLevel, seededAll);
|
|
1973
2057
|
|
|
1974
2058
|
if (seededQueue.length === 0) {
|
|
1975
2059
|
this.log('info', `[QUEST] Level ${targetLevel} tasks already complete from progress cache — verifying unlock`);
|
|
@@ -1997,6 +2081,71 @@ class AccountWorker {
|
|
|
1997
2081
|
return true;
|
|
1998
2082
|
}
|
|
1999
2083
|
|
|
2084
|
+
_getCommandDefaultCooldownSec(cmdName) {
|
|
2085
|
+
const cmd = String(cmdName || '').toLowerCase().trim();
|
|
2086
|
+
if (!cmd) return 8;
|
|
2087
|
+
const map = AccountWorker.COMMAND_MAP || [];
|
|
2088
|
+
const exact = map.find(r => String(r.cmd || '').toLowerCase() === cmd);
|
|
2089
|
+
if (exact && Number.isFinite(exact.defaultCd) && exact.defaultCd > 0) return exact.defaultCd;
|
|
2090
|
+
if (cmd === 'highlow') {
|
|
2091
|
+
const hl = map.find(r => String(r.cmd || '').toLowerCase() === 'hl');
|
|
2092
|
+
if (hl?.defaultCd) return hl.defaultCd;
|
|
2093
|
+
}
|
|
2094
|
+
const head = cmd.split(/\s+/)[0];
|
|
2095
|
+
const byHead = map.find(r => String(r.cmd || '').toLowerCase() === head);
|
|
2096
|
+
if (byHead && Number.isFinite(byHead.defaultCd) && byHead.defaultCd > 0) return byHead.defaultCd;
|
|
2097
|
+
return 10;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
_initLevelQuestRunState(targetLevel, questRows) {
|
|
2101
|
+
const startedAt = Date.now();
|
|
2102
|
+
const state = {
|
|
2103
|
+
level: targetLevel,
|
|
2104
|
+
startedAt,
|
|
2105
|
+
updatedAt: startedAt,
|
|
2106
|
+
completedAt: 0,
|
|
2107
|
+
entries: new Map(),
|
|
2108
|
+
};
|
|
2109
|
+
|
|
2110
|
+
for (const q of (questRows || [])) {
|
|
2111
|
+
const cmd = String(q.cmd || '').toLowerCase();
|
|
2112
|
+
if (!cmd) continue;
|
|
2113
|
+
const required = q.loseTarget
|
|
2114
|
+
? Number(q.loseTarget || 0)
|
|
2115
|
+
: Math.max(0, Number(q._doneSoFar || 0) + Number(q.times || 0));
|
|
2116
|
+
const completed = q.loseTarget
|
|
2117
|
+
? Math.max(0, Number(q._lostSoFar || 0))
|
|
2118
|
+
: Math.max(0, Number(q._doneSoFar || 0));
|
|
2119
|
+
const remaining = q.loseTarget
|
|
2120
|
+
? Math.max(0, required - completed)
|
|
2121
|
+
: Math.max(0, Number(q.times || 0));
|
|
2122
|
+
|
|
2123
|
+
state.entries.set(cmd, {
|
|
2124
|
+
cmd,
|
|
2125
|
+
required,
|
|
2126
|
+
completed,
|
|
2127
|
+
remaining,
|
|
2128
|
+
attemptsLeft: Number(q.times || 0),
|
|
2129
|
+
runs: 0,
|
|
2130
|
+
success: 0,
|
|
2131
|
+
failed: 0,
|
|
2132
|
+
nonPlay: 0,
|
|
2133
|
+
lastResult: '',
|
|
2134
|
+
lastRunAt: 0,
|
|
2135
|
+
nextRunAt: Number(q._nextRunAt || 0),
|
|
2136
|
+
status: remaining <= 0 ? 'done' : 'pending',
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
this._levelQuestRunMap.set(targetLevel, state);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
_getLevelQuestRunEntry(level, cmdName) {
|
|
2144
|
+
const st = this._levelQuestRunMap.get(level);
|
|
2145
|
+
if (!st?.entries) return null;
|
|
2146
|
+
return st.entries.get(String(cmdName || '').toLowerCase()) || null;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2000
2149
|
async buildCommandQueue() {
|
|
2001
2150
|
const heap = new MinHeap();
|
|
2002
2151
|
const now = Date.now();
|
|
@@ -2259,6 +2408,11 @@ class AccountWorker {
|
|
|
2259
2408
|
this.log('success', `[QUEST] Level ${targetLv} verified ✓ — level unlocked!`);
|
|
2260
2409
|
this._level = currentLv;
|
|
2261
2410
|
this._levelQuestProgressCache.delete(targetLv);
|
|
2411
|
+
const st = this._levelQuestRunMap.get(targetLv);
|
|
2412
|
+
if (st) {
|
|
2413
|
+
st.updatedAt = Date.now();
|
|
2414
|
+
st.verifiedAt = Date.now();
|
|
2415
|
+
}
|
|
2262
2416
|
} else {
|
|
2263
2417
|
this.log('warn', `[QUEST] Level ${targetLv} NOT verified — still at ${currentLv}. Re-triggering quests.`);
|
|
2264
2418
|
// Re-trigger quests for this level
|
|
@@ -2279,7 +2433,22 @@ class AccountWorker {
|
|
|
2279
2433
|
|
|
2280
2434
|
// BLOCK: quest mode active — run quests only, no normal grinding
|
|
2281
2435
|
if (this._levelQuestActive && this._levelQuestQueue.length > 0) {
|
|
2436
|
+
const now = Date.now();
|
|
2437
|
+
let dueIdx = this._levelQuestQueue.findIndex(q => !q._nextRunAt || q._nextRunAt <= now);
|
|
2438
|
+
if (dueIdx < 0) {
|
|
2439
|
+
const soonest = Math.min(...this._levelQuestQueue.map(q => Number(q._nextRunAt || now + 5000)));
|
|
2440
|
+
const waitMs = Math.max(1200, Math.min(60000, soonest - now));
|
|
2441
|
+
this.setStatus(`[QUEST] cooldown wait ${Math.ceil(waitMs / 1000)}s`);
|
|
2442
|
+
this.tickTimeout = setTimeout(() => this.tick(), waitMs);
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
if (dueIdx > 0) {
|
|
2446
|
+
const [dueQuest] = this._levelQuestQueue.splice(dueIdx, 1);
|
|
2447
|
+
this._levelQuestQueue.unshift(dueQuest);
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2282
2450
|
const quest = this._levelQuestQueue[0];
|
|
2451
|
+
let nextQuestDelayMs = 1200;
|
|
2283
2452
|
if (quest.loseTarget) {
|
|
2284
2453
|
const remaining = Math.max(0, quest.loseTarget - (quest._lostSoFar || 0));
|
|
2285
2454
|
const floorBet = quest._minBet || quest.bet || 1;
|
|
@@ -2306,6 +2475,12 @@ class AccountWorker {
|
|
|
2306
2475
|
this.busy = true;
|
|
2307
2476
|
await this.runCommand(quest.cmd, prefix);
|
|
2308
2477
|
const questMeta = this._lastCommandMeta || {};
|
|
2478
|
+
const qEntry = this._getLevelQuestRunEntry(quest.level, quest.cmd);
|
|
2479
|
+
if (qEntry) {
|
|
2480
|
+
qEntry.runs++;
|
|
2481
|
+
qEntry.lastRunAt = Date.now();
|
|
2482
|
+
qEntry.lastResult = String(questMeta.result || '');
|
|
2483
|
+
}
|
|
2309
2484
|
this._commandRunning = false;
|
|
2310
2485
|
this.busy = false;
|
|
2311
2486
|
this._questBetOverride = null;
|
|
@@ -2328,6 +2503,10 @@ class AccountWorker {
|
|
|
2328
2503
|
if (!nonPlay) {
|
|
2329
2504
|
quest._lostSoFar = (quest._lostSoFar || 0) + lostThisRound;
|
|
2330
2505
|
this._mergeLevelQuestProgress(quest.level, { [quest.cmd]: quest._lostSoFar });
|
|
2506
|
+
if (qEntry) qEntry.success++;
|
|
2507
|
+
} else if (qEntry) {
|
|
2508
|
+
qEntry.nonPlay++;
|
|
2509
|
+
qEntry.failed++;
|
|
2331
2510
|
}
|
|
2332
2511
|
|
|
2333
2512
|
this.log('info', `[QUEST] ${quest.cmd} — lost ⏣${quest._lostSoFar.toLocaleString()} / ⏣${quest.loseTarget.toLocaleString()}`);
|
|
@@ -2346,10 +2525,57 @@ class AccountWorker {
|
|
|
2346
2525
|
questStepDone = true;
|
|
2347
2526
|
this.log('info', `[QUEST] ${quest.cmd} max attempts reached — moving on`);
|
|
2348
2527
|
}
|
|
2528
|
+
|
|
2529
|
+
if (qEntry) {
|
|
2530
|
+
qEntry.completed = Math.max(0, Number(quest._lostSoFar || 0));
|
|
2531
|
+
qEntry.remaining = Math.max(0, Number(quest.loseTarget || 0) - qEntry.completed);
|
|
2532
|
+
qEntry.attemptsLeft = Number(quest.times || 0);
|
|
2533
|
+
qEntry.status = questStepDone ? 'done' : (nonPlay ? 'cooldown' : 'progress');
|
|
2534
|
+
}
|
|
2349
2535
|
} else {
|
|
2350
2536
|
// Normal times-based quest
|
|
2351
|
-
|
|
2537
|
+
const nonPlay = !!questMeta.nonPlay;
|
|
2538
|
+
if (!nonPlay) {
|
|
2539
|
+
quest.times--;
|
|
2540
|
+
quest._doneSoFar = (quest._doneSoFar || 0) + 1;
|
|
2541
|
+
this._mergeLevelQuestProgress(quest.level, { [quest.cmd]: quest._doneSoFar });
|
|
2542
|
+
if (qEntry) qEntry.success++;
|
|
2543
|
+
} else {
|
|
2544
|
+
if (qEntry) {
|
|
2545
|
+
qEntry.nonPlay++;
|
|
2546
|
+
qEntry.failed++;
|
|
2547
|
+
}
|
|
2548
|
+
this.log('warn', `[QUEST] ${quest.cmd} non-play response — progress unchanged`);
|
|
2549
|
+
// Rotate to next quest so we don't spam one command during cooldown.
|
|
2550
|
+
if (this._levelQuestQueue.length > 1) {
|
|
2551
|
+
const rotated = this._levelQuestQueue.shift();
|
|
2552
|
+
this._levelQuestQueue.push(rotated);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2352
2555
|
if (quest.times <= 0) questStepDone = true;
|
|
2556
|
+
|
|
2557
|
+
if (qEntry) {
|
|
2558
|
+
qEntry.completed = Math.max(0, Number(quest._doneSoFar || 0));
|
|
2559
|
+
qEntry.remaining = Math.max(0, Number(quest.times || 0));
|
|
2560
|
+
qEntry.attemptsLeft = Number(quest.times || 0);
|
|
2561
|
+
qEntry.status = questStepDone ? 'done' : (nonPlay ? 'cooldown' : 'progress');
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
const retrySec = Math.max(
|
|
2566
|
+
1,
|
|
2567
|
+
Number(questMeta.nextRetrySec || 0),
|
|
2568
|
+
Number(quest._defaultCdSec || 0)
|
|
2569
|
+
);
|
|
2570
|
+
quest._nextRunAt = Date.now() + retrySec * 1000;
|
|
2571
|
+
nextQuestDelayMs = Math.max(nextQuestDelayMs, Math.min(60000, Math.floor(retrySec * 1000)));
|
|
2572
|
+
if (qEntry) qEntry.nextRunAt = Number(quest._nextRunAt || 0);
|
|
2573
|
+
|
|
2574
|
+
const runState = this._levelQuestRunMap.get(quest.level);
|
|
2575
|
+
if (runState) runState.updatedAt = Date.now();
|
|
2576
|
+
|
|
2577
|
+
if (questMeta.nextRetrySec && Number.isFinite(questMeta.nextRetrySec) && questMeta.nextRetrySec > 0) {
|
|
2578
|
+
nextQuestDelayMs = Math.max(nextQuestDelayMs, Math.min(60000, Math.floor(questMeta.nextRetrySec * 1000)));
|
|
2353
2579
|
}
|
|
2354
2580
|
|
|
2355
2581
|
if (questStepDone) this._levelQuestQueue.shift();
|
|
@@ -2358,6 +2584,11 @@ class AccountWorker {
|
|
|
2358
2584
|
this._levelQuestActive = false;
|
|
2359
2585
|
this._levelQuestDone.add(quest.level);
|
|
2360
2586
|
const justCompletedLevel = quest.level;
|
|
2587
|
+
const st = this._levelQuestRunMap.get(justCompletedLevel);
|
|
2588
|
+
if (st) {
|
|
2589
|
+
st.completedAt = Date.now();
|
|
2590
|
+
st.updatedAt = st.completedAt;
|
|
2591
|
+
}
|
|
2361
2592
|
this.log('success', `[QUEST] Level ${justCompletedLevel} quests DONE — verifying unlock...`);
|
|
2362
2593
|
// Null out commandQueue so buildCommandQueue picks only unlocked commands
|
|
2363
2594
|
this.commandQueue = null;
|
|
@@ -2367,7 +2598,7 @@ class AccountWorker {
|
|
|
2367
2598
|
this.tickTimeout = setTimeout(() => this.tick(), 3000);
|
|
2368
2599
|
return;
|
|
2369
2600
|
}
|
|
2370
|
-
this.tickTimeout = setTimeout(() => this.tick(),
|
|
2601
|
+
this.tickTimeout = setTimeout(() => this.tick(), nextQuestDelayMs);
|
|
2371
2602
|
return;
|
|
2372
2603
|
}
|
|
2373
2604
|
|
package/lib/rawLogger.js
CHANGED
|
@@ -457,6 +457,20 @@ async function store(d, event) {
|
|
|
457
457
|
pipe.ltrim(accKey, 0, 4999);
|
|
458
458
|
pipe.expire(accKey, LOG_TTL);
|
|
459
459
|
}
|
|
460
|
+
// Track ephemeral in memory for quick access before they vanish
|
|
461
|
+
if (parsed.isEphemeral || (d.flags & 32832)) {
|
|
462
|
+
let chMap = ephemeralByChannel.get(d.channel_id);
|
|
463
|
+
if (!chMap) {
|
|
464
|
+
chMap = new Map();
|
|
465
|
+
ephemeralByChannel.set(d.channel_id, chMap);
|
|
466
|
+
}
|
|
467
|
+
chMap.set(d.id, parsed);
|
|
468
|
+
// Cap per-channel ephemeral buffer at 50
|
|
469
|
+
if (chMap.size > 50) {
|
|
470
|
+
const firstKey = chMap.keys().next().value;
|
|
471
|
+
chMap.delete(firstKey);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
460
474
|
// Ephemeral log
|
|
461
475
|
if (parsed.isEphemeral || (d.flags & 32832)) {
|
|
462
476
|
pipe.lpush('raw:ephemeral:log', `${d.id}:${parsed._capturedAt}:${parsed.command}:${d.channel_id}`);
|
|
@@ -494,8 +508,17 @@ function onDmEvent(fn) { dmListeners.push(fn); }
|
|
|
494
508
|
|
|
495
509
|
// ── Ephemeral message callbacks ──
|
|
496
510
|
const ephemeralListeners = [];
|
|
511
|
+
const ephemeralByChannel = new Map(); // channelId → Map<msgId, parsed msg>
|
|
512
|
+
|
|
497
513
|
function onNextEphemeral(fn) { ephemeralListeners.push(fn); }
|
|
498
514
|
|
|
515
|
+
// Track ephemeral messages per channel — useful for reading state before vanish
|
|
516
|
+
function getEphemeralMsgs(channelId, count = 20) {
|
|
517
|
+
const map = ephemeralByChannel.get(channelId);
|
|
518
|
+
if (!map) return [];
|
|
519
|
+
return [...map.values()].slice(-count);
|
|
520
|
+
}
|
|
521
|
+
|
|
499
522
|
function _notifyEphemeral(parsed) {
|
|
500
523
|
if (!ephemeralListeners.length) return;
|
|
501
524
|
const listeners = [...ephemeralListeners];
|