dankgrinder 5.23.0 → 5.25.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/antiDetect.js +211 -0
- package/lib/commands/adventure.js +36 -25
- package/lib/commands/dig.js +80 -15
- package/lib/commands/farm.js +14 -3
- package/lib/commands/inventory.js +20 -5
- package/lib/commands/stream.js +33 -3
- package/lib/commands/utils.js +20 -2
- package/lib/commands/work.js +37 -13
- package/lib/cooldownManager.js +347 -0
- package/lib/grinder.js +81 -32
- package/package.json +1 -1
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Anti-Detection System
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Adaptive human-like timing with context-aware delays
|
|
6
|
+
* - Micro-pause injection for natural interaction patterns
|
|
7
|
+
* - Activity-based variance (tired vs alert simulation)
|
|
8
|
+
* - Session behavior drift (mimics human consistency changes)
|
|
9
|
+
* - Randomized click timing within safe bounds
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
// ── Configuration ─────────────────────────────────────────────
|
|
15
|
+
const CONFIG = Object.freeze({
|
|
16
|
+
// Base delay ranges in ms (tuned for natural human timing)
|
|
17
|
+
MIN_DELAY: 500, // 0.5s minimum - natural human speed
|
|
18
|
+
MAX_DELAY: 1000, // 1.0s maximum - not too slow
|
|
19
|
+
|
|
20
|
+
// Context multipliers (subtle variations)
|
|
21
|
+
MULT_FIRST_ACTION: 1.2, // Slight hesitation on first action
|
|
22
|
+
MULT_REPEATED_ACTION: 0.85, // Slightly faster on repeated actions
|
|
23
|
+
MULT_HIGH_VALUE: 1.1, // Marginally slower for high-value actions
|
|
24
|
+
MULT_LOW_VALUE: 0.9, // Slightly faster for low-value actions
|
|
25
|
+
|
|
26
|
+
// Micro-pause settings (subtle human-like hesitation)
|
|
27
|
+
MICRO_PAUSE_CHANCE: 0.25, // 25% chance of micro-pause
|
|
28
|
+
MICRO_PAUSE_MIN: 50,
|
|
29
|
+
MICRO_PAUSE_MAX: 150,
|
|
30
|
+
|
|
31
|
+
// Minimal drift to avoid detectable patterns
|
|
32
|
+
DRIFT_FACTOR: 0.01,
|
|
33
|
+
DRIFT_MAX: 0.08, // Max 8% drift
|
|
34
|
+
|
|
35
|
+
// Pattern randomization
|
|
36
|
+
PATTERN_RESET_CHANCE: 0.2, // 20% chance to reset timing pattern
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ── Session State ─────────────────────────────────────────────
|
|
40
|
+
const sessionState = {
|
|
41
|
+
actionCount: 0,
|
|
42
|
+
lastActionTime: 0,
|
|
43
|
+
currentDrift: 0,
|
|
44
|
+
patternSeed: Math.random(),
|
|
45
|
+
activityLevel: 1.0, // Starts neutral
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ── Seeded Random for Reproducible Patterns ──────────────────
|
|
49
|
+
function seededRandom(seed) {
|
|
50
|
+
const x = Math.sin(seed) * 10000;
|
|
51
|
+
return x - Math.floor(x);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Adaptive Delay Calculation ────────────────────────────────
|
|
55
|
+
/**
|
|
56
|
+
* Calculate human-like delay based on context.
|
|
57
|
+
* @param {Object} options
|
|
58
|
+
* @param {string} options.context - Action context ('first', 'repeated', 'high-value', 'low-value', 'normal')
|
|
59
|
+
* @param {number} options.baseMin - Minimum delay override
|
|
60
|
+
* @param {number} options.baseMax - Maximum delay override
|
|
61
|
+
* @param {boolean} options.skipMicroPause - Force skip micro-pause
|
|
62
|
+
* @returns {Promise<number>} - Delay in ms
|
|
63
|
+
*/
|
|
64
|
+
async function calcAdaptiveDelay(options = {}) {
|
|
65
|
+
const {
|
|
66
|
+
context = 'normal',
|
|
67
|
+
baseMin = CONFIG.MIN_DELAY,
|
|
68
|
+
baseMax = CONFIG.MAX_DELAY,
|
|
69
|
+
skipMicroPause = false,
|
|
70
|
+
} = options;
|
|
71
|
+
|
|
72
|
+
sessionState.actionCount++;
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
|
|
75
|
+
// Time since last action affects speed (rush vs relaxed)
|
|
76
|
+
const timeSinceLast = now - sessionState.lastActionTime;
|
|
77
|
+
const rushFactor = timeSinceLast < 500 ? 0.8 : timeSinceLast > 5000 ? 1.15 : 1.0;
|
|
78
|
+
|
|
79
|
+
// Apply context multiplier
|
|
80
|
+
let multiplier = 1.0;
|
|
81
|
+
switch (context) {
|
|
82
|
+
case 'first':
|
|
83
|
+
multiplier = CONFIG.MULT_FIRST_ACTION;
|
|
84
|
+
break;
|
|
85
|
+
case 'repeated':
|
|
86
|
+
multiplier = CONFIG.MULT_REPEATED_ACTION;
|
|
87
|
+
break;
|
|
88
|
+
case 'high-value':
|
|
89
|
+
multiplier = CONFIG.MULT_HIGH_VALUE;
|
|
90
|
+
break;
|
|
91
|
+
case 'low-value':
|
|
92
|
+
multiplier = CONFIG.MULT_LOW_VALUE;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Apply drift (session behavior change)
|
|
97
|
+
sessionState.currentDrift += (Math.random() - 0.5) * CONFIG.DRIFT_FACTOR;
|
|
98
|
+
sessionState.currentDrift = Math.max(
|
|
99
|
+
-CONFIG.DRIFT_MAX,
|
|
100
|
+
Math.min(CONFIG.DRIFT_MAX, sessionState.currentDrift)
|
|
101
|
+
);
|
|
102
|
+
multiplier *= (1 + sessionState.currentDrift);
|
|
103
|
+
|
|
104
|
+
// Pattern reset for unpredictability
|
|
105
|
+
if (Math.random() < CONFIG.PATTERN_RESET_CHANCE) {
|
|
106
|
+
sessionState.patternSeed = Math.random();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Calculate base delay with seeded randomness
|
|
110
|
+
const seed = sessionState.patternSeed + (sessionState.actionCount * 0.001);
|
|
111
|
+
const randomFactor = 0.5 + seededRandom(seed); // 0.5 to 1.5 range
|
|
112
|
+
const baseDelay = baseMin + (baseMax - baseMin) * randomFactor;
|
|
113
|
+
|
|
114
|
+
// Apply all modifiers
|
|
115
|
+
let finalDelay = baseDelay * multiplier * rushFactor;
|
|
116
|
+
|
|
117
|
+
// Add micro-pause (simulates human hesitation)
|
|
118
|
+
if (!skipMicroPause && Math.random() < CONFIG.MICRO_PAUSE_CHANCE) {
|
|
119
|
+
const microPause = CONFIG.MICRO_PAUSE_MIN +
|
|
120
|
+
(CONFIG.MICRO_PAUSE_MAX - CONFIG.MICRO_PAUSE_MIN) * seededRandom(seed + 0.5);
|
|
121
|
+
finalDelay += microPause;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Clamp to reasonable bounds
|
|
125
|
+
finalDelay = Math.max(50, Math.min(800, finalDelay));
|
|
126
|
+
|
|
127
|
+
sessionState.lastActionTime = Date.now();
|
|
128
|
+
|
|
129
|
+
return Math.round(finalDelay);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Main Human Delay Function ─────────────────────────────────
|
|
133
|
+
/**
|
|
134
|
+
* Wait with human-like timing.
|
|
135
|
+
* @param {Object} options - Same as calcAdaptiveDelay
|
|
136
|
+
*/
|
|
137
|
+
async function humanDelay(options = {}) {
|
|
138
|
+
const delay = await calcAdaptiveDelay(options);
|
|
139
|
+
return new Promise(resolve => setTimeout(resolve, delay));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Reset Session State ───────────────────────────────────────
|
|
143
|
+
function resetSession() {
|
|
144
|
+
sessionState.actionCount = 0;
|
|
145
|
+
sessionState.lastActionTime = 0;
|
|
146
|
+
sessionState.currentDrift = 0;
|
|
147
|
+
sessionState.patternSeed = Math.random();
|
|
148
|
+
sessionState.activityLevel = 1.0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Get Session Stats (for debugging) ─────────────────────────
|
|
152
|
+
function getSessionStats() {
|
|
153
|
+
return {
|
|
154
|
+
actionCount: sessionState.actionCount,
|
|
155
|
+
currentDrift: sessionState.currentDrift,
|
|
156
|
+
timeSinceLast: Date.now() - sessionState.lastActionTime,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Smart Cooldown Calculator ─────────────────────────────────
|
|
161
|
+
/**
|
|
162
|
+
* Calculate smart cooldown with safety buffer and variance.
|
|
163
|
+
* @param {number} baseCooldown - Base cooldown in seconds
|
|
164
|
+
* @param {Object} options
|
|
165
|
+
* @param {boolean} options.addBuffer - Add safety buffer
|
|
166
|
+
* @param {boolean} options.addVariance - Add random variance
|
|
167
|
+
* @returns {number} - Adjusted cooldown in seconds
|
|
168
|
+
*/
|
|
169
|
+
function calcSmartCooldown(baseCooldown, options = {}) {
|
|
170
|
+
const { addBuffer = true, addVariance = true } = options;
|
|
171
|
+
|
|
172
|
+
let adjusted = baseCooldown;
|
|
173
|
+
|
|
174
|
+
// Add safety buffer
|
|
175
|
+
if (addBuffer) {
|
|
176
|
+
adjusted += CONFIG.COOLDOWN_BUFFER_SEC;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Add small variance (±2%) to avoid detectable patterns
|
|
180
|
+
if (addVariance && baseCooldown > 10) {
|
|
181
|
+
const variance = (Math.random() - 0.5) * 0.04 * baseCooldown;
|
|
182
|
+
adjusted += variance;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return Math.max(5, Math.round(adjusted));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Exponential Backoff with Jitter ───────────────────────────
|
|
189
|
+
/**
|
|
190
|
+
* Calculate backoff delay for retries.
|
|
191
|
+
* @param {number} attempt - Attempt number (0-indexed)
|
|
192
|
+
* @param {number} baseMs - Base delay in ms
|
|
193
|
+
* @param {number} maxMs - Maximum delay in ms
|
|
194
|
+
* @returns {number} - Delay in ms
|
|
195
|
+
*/
|
|
196
|
+
function calcBackoff(attempt, baseMs = 1000, maxMs = 30000) {
|
|
197
|
+
const exponential = baseMs * Math.pow(2, attempt);
|
|
198
|
+
const jitter = exponential * 0.2 * (Math.random() - 0.5);
|
|
199
|
+
return Math.min(maxMs, Math.round(exponential + jitter));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Exports ───────────────────────────────────────────────────
|
|
203
|
+
module.exports = {
|
|
204
|
+
humanDelay,
|
|
205
|
+
calcAdaptiveDelay,
|
|
206
|
+
calcSmartCooldown,
|
|
207
|
+
calcBackoff,
|
|
208
|
+
resetSession,
|
|
209
|
+
getSessionStats,
|
|
210
|
+
CONFIG,
|
|
211
|
+
};
|
|
@@ -39,8 +39,7 @@ const {
|
|
|
39
39
|
safeClickButton, isHoldTight, logMsg,
|
|
40
40
|
} = require('./utils');
|
|
41
41
|
|
|
42
|
-
const RE_DISCORD_TIMESTAMP = /<t:(\d+)(?::[tTdDfFR])
|
|
43
|
-
const RE_ADVENTURE_AGAIN_LABEL = /adventure again in (\d+)\s*(minute|min|hour|second)/;
|
|
42
|
+
const RE_DISCORD_TIMESTAMP = /<t:(\d+)(?::[tTdDfFR])?>/g;
|
|
44
43
|
|
|
45
44
|
// ── Adventure type rotation (cycle through all types each run) ────
|
|
46
45
|
let lastAdventureIndex = -1;
|
|
@@ -270,7 +269,7 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
|
|
|
270
269
|
|
|
271
270
|
if (!response) {
|
|
272
271
|
LOG.warn('[adventure] No response from Dank Memer');
|
|
273
|
-
return { result: 'no response', coins: 0, nextCooldownSec:
|
|
272
|
+
return { result: 'no response', coins: 0, nextCooldownSec: 180 };
|
|
274
273
|
}
|
|
275
274
|
|
|
276
275
|
// Check for Hold Tight
|
|
@@ -439,33 +438,45 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
|
|
|
439
438
|
function buildResult(finalText, coins, interactions, rewards, msg) {
|
|
440
439
|
let nextCooldownSec = null;
|
|
441
440
|
|
|
442
|
-
// 1) Best:
|
|
443
|
-
|
|
444
|
-
const tsMatch = allText.match(RE_DISCORD_TIMESTAMP);
|
|
445
|
-
if (tsMatch) {
|
|
446
|
-
const unixTarget = parseInt(tsMatch[1]);
|
|
447
|
-
const nowUnix = Math.floor(Date.now() / 1000);
|
|
448
|
-
nextCooldownSec = Math.max(5, unixTarget - nowUnix);
|
|
449
|
-
LOG.info(`[adventure] Next at ${new Date(unixTarget * 1000).toLocaleTimeString()} (${c.yellow}${nextCooldownSec}s${c.reset})`);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// 2) Fallback: "Adventure again in X minutes" button label
|
|
453
|
-
if (!nextCooldownSec && msg) {
|
|
441
|
+
// 1) Best: button label after adventure ends (e.g. "Adventure again in 57m")
|
|
442
|
+
if (msg) {
|
|
454
443
|
const rows = msg.components || [];
|
|
455
444
|
for (let ri = 0; ri < rows.length; ri++) {
|
|
456
445
|
const comps = rows[ri].components || [];
|
|
457
446
|
for (let ci = 0; ci < comps.length; ci++) {
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
447
|
+
const label = String(comps[ci]?.label || '').toLowerCase();
|
|
448
|
+
if (!label.includes('adventure') || !label.includes('again')) continue;
|
|
449
|
+
const unitMatch = label.match(/(\d+)\s*(h|hr|hrs|hour|hours|m|min|mins|minute|minutes|s|sec|secs|second|seconds)\b/i);
|
|
450
|
+
if (!unitMatch) continue;
|
|
451
|
+
const n = parseInt(unitMatch[1], 10);
|
|
452
|
+
const u = unitMatch[2].toLowerCase();
|
|
453
|
+
if (!Number.isFinite(n) || n <= 0) continue;
|
|
454
|
+
if (u.startsWith('h')) nextCooldownSec = n * 3600;
|
|
455
|
+
else if (u.startsWith('m')) nextCooldownSec = n * 60;
|
|
456
|
+
else nextCooldownSec = n;
|
|
457
|
+
LOG.info(`[adventure] Next cooldown from button: ${c.yellow}${nextCooldownSec}s${c.reset}`);
|
|
458
|
+
break;
|
|
468
459
|
}
|
|
460
|
+
if (nextCooldownSec) break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 2) Fallback: Unix timestamp <t:UNIX:*> in final text or embed
|
|
465
|
+
const allText = msg ? getFullText(msg) : finalText;
|
|
466
|
+
if (!nextCooldownSec) {
|
|
467
|
+
const nowUnix = Math.floor(Date.now() / 1000);
|
|
468
|
+
const tsMatches = Array.from(String(allText || '').matchAll(RE_DISCORD_TIMESTAMP));
|
|
469
|
+
let best = null;
|
|
470
|
+
for (const m of tsMatches) {
|
|
471
|
+
const unixTarget = parseInt(m[1], 10);
|
|
472
|
+
if (!Number.isFinite(unixTarget)) continue;
|
|
473
|
+
const diff = unixTarget - nowUnix;
|
|
474
|
+
if (diff <= 0) continue;
|
|
475
|
+
if (best == null || diff < best) best = diff;
|
|
476
|
+
}
|
|
477
|
+
if (best != null) {
|
|
478
|
+
nextCooldownSec = Math.max(5, best);
|
|
479
|
+
LOG.info(`[adventure] Next cooldown from timestamp: ${c.yellow}${nextCooldownSec}s${c.reset}`);
|
|
469
480
|
}
|
|
470
481
|
}
|
|
471
482
|
|
package/lib/commands/dig.js
CHANGED
|
@@ -4,11 +4,88 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const {
|
|
7
|
-
LOG, c, getFullText, parseCoins,
|
|
7
|
+
LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton, humanDelay,
|
|
8
|
+
logMsg, isHoldTight, getHoldTightReason, sleep, needsItem,
|
|
8
9
|
isCV2, ensureCV2, stripAnsi,
|
|
9
10
|
} = require('./utils');
|
|
10
11
|
const { buyItem } = require('./shop');
|
|
11
12
|
|
|
13
|
+
const DIG_LANE_LABELS = new Set(['left', 'middle', 'right']);
|
|
14
|
+
|
|
15
|
+
async function waitForDigUpdate(channel, messageId, baselineText, waitForDankMemer, timeoutMs = 9000) {
|
|
16
|
+
const started = Date.now();
|
|
17
|
+
while (Date.now() - started < timeoutMs) {
|
|
18
|
+
const msg = await waitForDankMemer(2200);
|
|
19
|
+
if (msg && msg.id === messageId) {
|
|
20
|
+
if (isCV2(msg)) await ensureCV2(msg, true);
|
|
21
|
+
const txt = stripAnsi(getFullText(msg)).replace(/\s+/g, ' ').trim();
|
|
22
|
+
if (!baselineText || (txt && txt !== baselineText)) return msg;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await sleep(350);
|
|
26
|
+
try {
|
|
27
|
+
const fresh = await channel.messages.fetch(messageId);
|
|
28
|
+
if (!fresh) continue;
|
|
29
|
+
if (isCV2(fresh)) await ensureCV2(fresh, true);
|
|
30
|
+
const txt = stripAnsi(getFullText(fresh)).replace(/\s+/g, ' ').trim();
|
|
31
|
+
if (!baselineText || (txt && txt !== baselineText)) return fresh;
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function pickDigLaneButton(msg) {
|
|
38
|
+
const buttons = getAllButtons(msg).filter((b) => !b.disabled && DIG_LANE_LABELS.has(String(b.label || '').toLowerCase()));
|
|
39
|
+
if (buttons.length === 0) return null;
|
|
40
|
+
|
|
41
|
+
const textLower = stripAnsi(getFullText(msg)).replace(/\s+/g, ' ').trim().toLowerCase();
|
|
42
|
+
|
|
43
|
+
// If prompt text explicitly mentions a lane, follow it.
|
|
44
|
+
for (const lane of DIG_LANE_LABELS) {
|
|
45
|
+
if (new RegExp(`\\b${lane}\\b`, 'i').test(textLower)) {
|
|
46
|
+
const matched = buttons.find((b) => String(b.label || '').toLowerCase() === lane);
|
|
47
|
+
if (matched) return matched;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Fallback: random lane (still better than skipping the minigame).
|
|
52
|
+
return buttons[Math.floor(Math.random() * buttons.length)];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function resolveDigFlow({ channel, waitForDankMemer, response }) {
|
|
56
|
+
let current = response;
|
|
57
|
+
|
|
58
|
+
for (let round = 0; round < 3; round++) {
|
|
59
|
+
if (isCV2(current)) await ensureCV2(current, true);
|
|
60
|
+
const laneBtn = pickDigLaneButton(current);
|
|
61
|
+
if (!laneBtn) break;
|
|
62
|
+
|
|
63
|
+
LOG.info(`[dig] Minigame: clicking "${laneBtn.label}"`);
|
|
64
|
+
const baseline = stripAnsi(getFullText(current)).replace(/\s+/g, ' ').trim();
|
|
65
|
+
await humanDelay(110, 240);
|
|
66
|
+
|
|
67
|
+
let clicked = null;
|
|
68
|
+
try { clicked = await safeClickButton(current, laneBtn); } catch {}
|
|
69
|
+
if (clicked) current = clicked;
|
|
70
|
+
|
|
71
|
+
const updated = await waitForDigUpdate(channel, current.id, baseline, waitForDankMemer, 10000);
|
|
72
|
+
if (updated) {
|
|
73
|
+
current = updated;
|
|
74
|
+
logMsg(current, `dig-minigame-${round + 1}`);
|
|
75
|
+
} else {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const cleanText = stripAnsi(getFullText(current)).replace(/\s+/g, ' ').trim();
|
|
81
|
+
const coins = parseCoins(cleanText);
|
|
82
|
+
if (coins > 0) {
|
|
83
|
+
LOG.coin(`[dig] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
84
|
+
return { result: `dig → +⏣ ${coins.toLocaleString()}`, coins };
|
|
85
|
+
}
|
|
86
|
+
return { result: cleanText.substring(0, 90) || 'done', coins: 0 };
|
|
87
|
+
}
|
|
88
|
+
|
|
12
89
|
/**
|
|
13
90
|
* @param {object} opts
|
|
14
91
|
* @param {object} opts.channel
|
|
@@ -66,25 +143,13 @@ async function runDig({ channel, waitForDankMemer, client }) {
|
|
|
66
143
|
if (r2) {
|
|
67
144
|
if (isCV2(r2)) await ensureCV2(r2);
|
|
68
145
|
logMsg(r2, 'dig-retry');
|
|
69
|
-
|
|
70
|
-
const coins = parseCoins(t2);
|
|
71
|
-
if (coins > 0) {
|
|
72
|
-
LOG.coin(`[dig] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
73
|
-
return { result: `dig → +⏣ ${coins.toLocaleString()}`, coins };
|
|
74
|
-
}
|
|
75
|
-
return { result: stripAnsi(t2).replace(/\s+/g, ' ').trim().substring(0, 60), coins: 0 };
|
|
146
|
+
return await resolveDigFlow({ channel, waitForDankMemer, response: r2 });
|
|
76
147
|
}
|
|
77
148
|
}
|
|
78
149
|
return { result: 'need shovel (buy failed)', coins: 0 };
|
|
79
150
|
}
|
|
80
151
|
|
|
81
|
-
|
|
82
|
-
if (coins > 0) {
|
|
83
|
-
LOG.coin(`[dig] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
84
|
-
return { result: `dig → +⏣ ${coins.toLocaleString()}`, coins };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return { result: cleanText.substring(0, 60) || 'done', coins: 0 };
|
|
152
|
+
return await resolveDigFlow({ channel, waitForDankMemer, response });
|
|
88
153
|
}
|
|
89
154
|
|
|
90
155
|
module.exports = { runDig };
|
package/lib/commands/farm.js
CHANGED
|
@@ -62,6 +62,13 @@ function parseFarmGrowReadySec(text) {
|
|
|
62
62
|
if (tsNearReady.length > 0) return Math.max(5, Math.min(...tsNearReady));
|
|
63
63
|
|
|
64
64
|
// Fallback textual patterns.
|
|
65
|
+
const willReadyH = clean.match(/(?:will\s+be\s+)?ready\s+in\s+(\d+)\s*hour/i);
|
|
66
|
+
if (willReadyH) return Math.max(5, parseInt(willReadyH[1], 10) * 3600);
|
|
67
|
+
const willReadyM = clean.match(/(?:will\s+be\s+)?ready\s+in\s+(\d+)\s*minute/i);
|
|
68
|
+
if (willReadyM) return Math.max(5, parseInt(willReadyM[1], 10) * 60);
|
|
69
|
+
const willReadyS = clean.match(/(?:will\s+be\s+)?ready\s+in\s+(\d+)\s*second/i);
|
|
70
|
+
if (willReadyS) return Math.max(5, parseInt(willReadyS[1], 10));
|
|
71
|
+
|
|
65
72
|
const h = clean.match(/ready\s+in\s+(\d+)\s*hour/i);
|
|
66
73
|
if (h) return Math.max(5, parseInt(h[1], 10) * 3600);
|
|
67
74
|
const mi = clean.match(/ready\s+in\s+(\d+)\s*minute/i);
|
|
@@ -977,7 +984,7 @@ async function runFarm({ channel, waitForDankMemer, client, _buyRetryDepth = 0,
|
|
|
977
984
|
|
|
978
985
|
if (!response) {
|
|
979
986
|
LOG.warn('[farm] No response');
|
|
980
|
-
return { result: 'no response', coins: 0 };
|
|
987
|
+
return { result: 'no response', coins: 0, nextCooldownSec: 90 };
|
|
981
988
|
}
|
|
982
989
|
|
|
983
990
|
if (isHoldTight(response)) {
|
|
@@ -1022,7 +1029,7 @@ async function runFarm({ channel, waitForDankMemer, client, _buyRetryDepth = 0,
|
|
|
1022
1029
|
LOG.warn('[farm] Subcommand required response detected; retrying with "pls farm view"');
|
|
1023
1030
|
await channel.send('pls farm view');
|
|
1024
1031
|
const retry = await waitForDankMemer(12000);
|
|
1025
|
-
|
|
1032
|
+
if (!retry) return { result: 'no response after farm view retry', coins: 0, nextCooldownSec: 120 };
|
|
1026
1033
|
response = retry;
|
|
1027
1034
|
if (isCV2(response)) await ensureCV2(response);
|
|
1028
1035
|
logMsg(response, 'farm-retry-view');
|
|
@@ -1505,7 +1512,11 @@ async function runFarm({ channel, waitForDankMemer, client, _buyRetryDepth = 0,
|
|
|
1505
1512
|
}
|
|
1506
1513
|
|
|
1507
1514
|
const coins = parseCoins(text);
|
|
1508
|
-
let nextCd = parseFarmCooldownSec(text) ||
|
|
1515
|
+
let nextCd = parseFarmCooldownSec(text) || 30;
|
|
1516
|
+
const growReadyEnd = parseFarmGrowReadySec(text);
|
|
1517
|
+
if (Number.isFinite(growReadyEnd) && growReadyEnd > 0) {
|
|
1518
|
+
nextCd = Math.max(nextCd, Math.min(6 * 3600, growReadyEnd + 2));
|
|
1519
|
+
}
|
|
1509
1520
|
const missingEnd = parseMissingFarmItem(text);
|
|
1510
1521
|
if (missingEnd) {
|
|
1511
1522
|
LOG.warn(`[farm] Missing ${c.bold}${missingEnd}${c.reset} after action — will retry in 1h`);
|
|
@@ -233,6 +233,7 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
233
233
|
allItems.push(...parseInventoryPage(response));
|
|
234
234
|
|
|
235
235
|
let guard = 0;
|
|
236
|
+
let noChangeCount = 0;
|
|
236
237
|
while (page < total && guard < Math.max(20, total + 6)) {
|
|
237
238
|
guard++;
|
|
238
239
|
const buttons = getAllButtons(response);
|
|
@@ -312,12 +313,17 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
312
313
|
delete response._cv2buttons;
|
|
313
314
|
|
|
314
315
|
let pageChanged = false;
|
|
315
|
-
for (let attempt = 0; attempt <
|
|
316
|
-
await sleep(attempt === 0 ?
|
|
316
|
+
for (let attempt = 0; attempt < 7; attempt++) {
|
|
317
|
+
await sleep(attempt === 0 ? 700 : 1100 + Math.min(attempt * 150, 600));
|
|
317
318
|
try {
|
|
318
|
-
const fresh = await
|
|
319
|
+
const fresh = await response.fetch(true);
|
|
319
320
|
if (fresh) response = fresh;
|
|
320
|
-
} catch {
|
|
321
|
+
} catch {
|
|
322
|
+
try {
|
|
323
|
+
const fresh = await channel.messages.fetch(response.id);
|
|
324
|
+
if (fresh) response = fresh;
|
|
325
|
+
} catch {}
|
|
326
|
+
}
|
|
321
327
|
if (isCV2(response)) await ensureCV2(response, true);
|
|
322
328
|
const pageInfo = parsePageInfo(response);
|
|
323
329
|
if (pageInfo.page > page && !visitedPages.has(pageInfo.page)) {
|
|
@@ -337,7 +343,16 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
337
343
|
delete response._cv2buttons;
|
|
338
344
|
}
|
|
339
345
|
|
|
340
|
-
if (!pageChanged)
|
|
346
|
+
if (!pageChanged) {
|
|
347
|
+
noChangeCount++;
|
|
348
|
+
if (noChangeCount < 3) {
|
|
349
|
+
LOG.warn(`[inv] Page stuck at ${page}/${total}; retrying paginator click (${noChangeCount}/2)`);
|
|
350
|
+
await sleep(900 + Math.floor(Math.random() * 500));
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
noChangeCount = 0;
|
|
341
356
|
allItems.push(...parseInventoryPage(response));
|
|
342
357
|
}
|
|
343
358
|
|
package/lib/commands/stream.js
CHANGED
|
@@ -8,6 +8,7 @@ const { buyItem } = require('./shop');
|
|
|
8
8
|
const STREAM_ITEMS = Object.freeze(['keyboard', 'mouse']);
|
|
9
9
|
const STREAM_ACTION_LABELS = Object.freeze(['run ad', 'read chat', 'collect donations']);
|
|
10
10
|
const RE_STREAM_INTERACT_MIN = /interact\s+with\s+your\s+stream\s+every\s+`?(\d+)`?\s*minutes?/i;
|
|
11
|
+
const RE_STREAM_TS = /<t:(\d+)(?::[tTdDfFR])?>/g;
|
|
11
12
|
|
|
12
13
|
function normalizeLower(text) {
|
|
13
14
|
return String(text || '')
|
|
@@ -123,11 +124,40 @@ function isActionResultText(lowerText) {
|
|
|
123
124
|
|
|
124
125
|
function parseStreamInteractCooldownSec(text) {
|
|
125
126
|
const clean = String(stripAnsi(text || '')).replace(/\s+/g, ' ').trim();
|
|
127
|
+
|
|
128
|
+
// Primary: explicit interaction cadence from dashboard text.
|
|
126
129
|
const mm = clean.match(RE_STREAM_INTERACT_MIN);
|
|
127
130
|
if (mm) {
|
|
128
131
|
const mins = parseInt(mm[1], 10);
|
|
129
132
|
if (Number.isFinite(mins) && mins > 0) return mins * 60;
|
|
130
133
|
}
|
|
134
|
+
|
|
135
|
+
// Secondary: derive from "last streamed" marker (10m base cooldown).
|
|
136
|
+
const lower = normalizeLower(clean);
|
|
137
|
+
const now = Math.floor(Date.now() / 1000);
|
|
138
|
+
const tsMatches = Array.from(clean.matchAll(RE_STREAM_TS));
|
|
139
|
+
for (const m of tsMatches) {
|
|
140
|
+
const ts = parseInt(m[1], 10);
|
|
141
|
+
if (!Number.isFinite(ts) || ts <= 0) continue;
|
|
142
|
+
const idx = m.index ?? 0;
|
|
143
|
+
const left = lower.slice(Math.max(0, idx - 70), idx);
|
|
144
|
+
if (!left.includes('last streamed')) continue;
|
|
145
|
+
const elapsed = Math.max(0, now - ts);
|
|
146
|
+
const remain = Math.max(5, 600 - elapsed);
|
|
147
|
+
return remain;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Textual fallback: "last streamed X minutes ago".
|
|
151
|
+
const ago = lower.match(/last\s+streamed[^\d]{0,24}(\d+)\s*(hour|hr|hrs|minute|min|mins|second|sec|secs)\s+ago/i);
|
|
152
|
+
if (ago) {
|
|
153
|
+
const n = parseInt(ago[1], 10);
|
|
154
|
+
const unit = ago[2].toLowerCase();
|
|
155
|
+
if (Number.isFinite(n) && n >= 0) {
|
|
156
|
+
const elapsed = unit.startsWith('h') ? n * 3600 : unit.startsWith('m') ? n * 60 : n;
|
|
157
|
+
return Math.max(5, 600 - elapsed);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
131
161
|
return 600;
|
|
132
162
|
}
|
|
133
163
|
|
|
@@ -171,7 +201,7 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
171
201
|
|
|
172
202
|
if (!response) {
|
|
173
203
|
LOG.warn('[stream] No response');
|
|
174
|
-
return { result: 'no response', coins: 0 };
|
|
204
|
+
return { result: 'no response', coins: 0, nextCooldownSec: 120 };
|
|
175
205
|
}
|
|
176
206
|
|
|
177
207
|
if (isHoldTight(response)) {
|
|
@@ -199,14 +229,14 @@ async function runStream({ channel, waitForDankMemer, client }) {
|
|
|
199
229
|
|
|
200
230
|
for (const item of itemsToBuy) {
|
|
201
231
|
const bought = await buyItem({ channel, waitForDankMemer, client, itemName: item, quantity: 1 });
|
|
202
|
-
if (!bought) return { result: `need ${item} (buy failed)`, coins: 0 };
|
|
232
|
+
if (!bought) return { result: `need ${item} (buy failed)`, coins: 0, nextCooldownSec: 1800 };
|
|
203
233
|
await humanDelay(500, 1000);
|
|
204
234
|
}
|
|
205
235
|
|
|
206
236
|
await sleep(2000);
|
|
207
237
|
await channel.send('pls stream');
|
|
208
238
|
response = await waitForDankMemer(12000);
|
|
209
|
-
|
|
239
|
+
if (!response) return { result: 'no response after buy', coins: 0, nextCooldownSec: 180 };
|
|
210
240
|
await hydrate(response);
|
|
211
241
|
logMsg(response, 'stream-retry');
|
|
212
242
|
text = getFullText(response);
|
package/lib/commands/utils.js
CHANGED
|
@@ -94,8 +94,26 @@ const RE_HOLD_TIGHT_REASON = /Reason:\s*\/(\w+)/i;
|
|
|
94
94
|
// ── Sleep ────────────────────────────────────────────────────
|
|
95
95
|
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
// ── Advanced Human Delay (Anti-Detection) ────────────────────
|
|
98
|
+
// Natural human-like timing: 0.5-1.0s base with subtle variations
|
|
99
|
+
let _antiDetect = null;
|
|
100
|
+
try {
|
|
101
|
+
_antiDetect = require('../antiDetect');
|
|
102
|
+
} catch {}
|
|
103
|
+
|
|
104
|
+
function humanDelay(contextOrMin, max) {
|
|
105
|
+
// Backward compatibility: if called with numeric args, use simple delay
|
|
106
|
+
if (typeof contextOrMin === 'number') {
|
|
107
|
+
const min = contextOrMin;
|
|
108
|
+
const maxVal = typeof max === 'number' ? max : 1000;
|
|
109
|
+
return new Promise(r => setTimeout(r, min + Math.random() * (maxVal - min)));
|
|
110
|
+
}
|
|
111
|
+
// New API: context-based adaptive delay (uses 0.5-1s range)
|
|
112
|
+
if (_antiDetect && typeof contextOrMin === 'object') {
|
|
113
|
+
return _antiDetect.humanDelay(contextOrMin);
|
|
114
|
+
}
|
|
115
|
+
// Default: natural human timing (0.5-1.0s)
|
|
116
|
+
return new Promise(r => setTimeout(r, 500 + Math.random() * 500));
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
// ── Message Parsing ──────────────────────────────────────────
|
package/lib/commands/work.js
CHANGED
|
@@ -23,7 +23,7 @@ const {
|
|
|
23
23
|
|
|
24
24
|
const RE_MEMORY_BACKTICK_CHUNK = /`([^`]+)`/g;
|
|
25
25
|
const RE_BACKTICK_STRIP = /`/g;
|
|
26
|
-
const RE_WORK_COOLDOWN_TS = /<t:(\d+)
|
|
26
|
+
const RE_WORK_COOLDOWN_TS = /<t:(\d+)(?::[tTdDfFR])?>/g;
|
|
27
27
|
const RE_WORK_COOLDOWN_MINUTES = /(\d+)\s*minute/i;
|
|
28
28
|
const RE_WORK_COOLDOWN_HOURS = /(\d+)\s*hour/i;
|
|
29
29
|
|
|
@@ -78,18 +78,42 @@ function parseMemoryOrder(text) {
|
|
|
78
78
|
* Patterns: "next shift <t:TIMESTAMP:R>", "X minutes", "X hours"
|
|
79
79
|
*/
|
|
80
80
|
function parseWorkCooldown(text) {
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
const clean = String(stripAnsi(text || ''));
|
|
82
|
+
const lower = normalizeLower(clean).replace(/\s+/g, ' ').trim();
|
|
83
|
+
|
|
84
|
+
// Unix timestamp pattern: <t:TIMESTAMP:*>
|
|
85
|
+
const now = Math.floor(Date.now() / 1000);
|
|
86
|
+
const tsMatches = Array.from(clean.matchAll(RE_WORK_COOLDOWN_TS));
|
|
87
|
+
if (tsMatches.length > 0) {
|
|
88
|
+
let best = null;
|
|
89
|
+
for (const m of tsMatches) {
|
|
90
|
+
const ts = parseInt(m[1], 10);
|
|
91
|
+
if (!Number.isFinite(ts)) continue;
|
|
92
|
+
const diff = ts - now;
|
|
93
|
+
if (diff <= 0) continue;
|
|
94
|
+
if (best == null || diff < best) best = diff;
|
|
95
|
+
}
|
|
96
|
+
if (best != null) return Math.max(5, best);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Compact forms frequently seen in buttons/messages: "1h", "54m", "30s"
|
|
100
|
+
if (/(next shift|work again|cooldown|already done|shift)/i.test(lower)) {
|
|
101
|
+
const short = lower.match(/(\d+)\s*(h|hr|hrs|m|min|mins|s|sec|secs)\b/i);
|
|
102
|
+
if (short) {
|
|
103
|
+
const n = parseInt(short[1], 10);
|
|
104
|
+
const u = short[2].toLowerCase();
|
|
105
|
+
if (Number.isFinite(n) && n > 0) {
|
|
106
|
+
if (u.startsWith('h')) return n * 3600;
|
|
107
|
+
if (u.startsWith('m')) return n * 60;
|
|
108
|
+
return n;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
88
111
|
}
|
|
112
|
+
|
|
89
113
|
// "X minutes" / "X hours" pattern
|
|
90
|
-
const minMatch =
|
|
114
|
+
const minMatch = clean.match(RE_WORK_COOLDOWN_MINUTES);
|
|
91
115
|
if (minMatch) return parseInt(minMatch[1]) * 60;
|
|
92
|
-
const hrMatch =
|
|
116
|
+
const hrMatch = clean.match(RE_WORK_COOLDOWN_HOURS);
|
|
93
117
|
if (hrMatch) return parseInt(hrMatch[1]) * 3600;
|
|
94
118
|
return null;
|
|
95
119
|
}
|
|
@@ -311,7 +335,7 @@ async function runWorkShift({ channel, waitForDankMemer }) {
|
|
|
311
335
|
|
|
312
336
|
if (!current) {
|
|
313
337
|
LOG.warn('[work] No response');
|
|
314
|
-
return { result: 'no response', coins: 0 };
|
|
338
|
+
return { result: 'no response', coins: 0, nextCooldownSec: 120 };
|
|
315
339
|
}
|
|
316
340
|
|
|
317
341
|
if (isHoldTight(current)) {
|
|
@@ -351,7 +375,7 @@ async function runWorkShift({ channel, waitForDankMemer }) {
|
|
|
351
375
|
|
|
352
376
|
await channel.send('pls work shift');
|
|
353
377
|
current = await waitForDankMemer(10000);
|
|
354
|
-
|
|
378
|
+
if (!current) return { result: 'no response after apply', coins: 0, nextCooldownSec: 180 };
|
|
355
379
|
if (isCV2(current)) await ensureCV2(current);
|
|
356
380
|
logMsg(current, 'work-after-apply');
|
|
357
381
|
text = getFullText(current);
|
|
@@ -425,7 +449,7 @@ async function runWorkShift({ channel, waitForDankMemer }) {
|
|
|
425
449
|
return { result: 'work shift completed', coins: 0, nextCooldownSec: finalCd || 3600 };
|
|
426
450
|
}
|
|
427
451
|
|
|
428
|
-
return { result: `work done`, coins: 0, nextCooldownSec: finalCd ||
|
|
452
|
+
return { result: `work done`, coins: 0, nextCooldownSec: finalCd || 1800 };
|
|
429
453
|
}
|
|
430
454
|
|
|
431
455
|
module.exports = { runWorkShift, autoApplyForJob, resignFromJob, JOBS };
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Cooldown Manager with Smart CD Calculations
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - EMA-based cooldown prediction (learns from history)
|
|
6
|
+
* - Command chaining with optimal timing
|
|
7
|
+
* - Priority queue for scheduling
|
|
8
|
+
* - Redis-backed shared state (cluster mode)
|
|
9
|
+
* - Predictive readiness estimation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const { LRUCache, MinHeap } = require('./structures');
|
|
15
|
+
const { calcSmartCooldown } = require('./antiDetect');
|
|
16
|
+
|
|
17
|
+
// ── Command Cooldown Config ───────────────────────────────────
|
|
18
|
+
const COMMAND_CONFIG = Object.freeze({
|
|
19
|
+
// Base cooldowns in seconds
|
|
20
|
+
beg: { base: 120, priority: 3, category: 'income' },
|
|
21
|
+
crime: { base: 1200, priority: 2, category: 'income' },
|
|
22
|
+
search: { base: 150, priority: 3, category: 'income' },
|
|
23
|
+
work: { base: 3600, priority: 1, category: 'income' },
|
|
24
|
+
dig: { base: 600, priority: 2, category: 'income' },
|
|
25
|
+
fish: { base: 900, priority: 2, category: 'income' },
|
|
26
|
+
hunt: { base: 120, priority: 3, category: 'income' },
|
|
27
|
+
farm: { base: 300, priority: 2, category: 'income' },
|
|
28
|
+
stream: { base: 120, priority: 3, category: 'income' },
|
|
29
|
+
trivia: { base: 180, priority: 2, category: 'income' },
|
|
30
|
+
highlow: { base: 120, priority: 2, category: 'income' },
|
|
31
|
+
gamble: { base: 30, priority: 1, category: 'gambling' },
|
|
32
|
+
blackjack: { base: 30, priority: 1, category: 'gambling' },
|
|
33
|
+
roulette: { base: 30, priority: 1, category: 'gambling' },
|
|
34
|
+
slots: { base: 30, priority: 1, category: 'gambling' },
|
|
35
|
+
adventure: { base: 1800, priority: 1, category: 'special' },
|
|
36
|
+
postmeme: { base: 300, priority: 2, category: 'social' },
|
|
37
|
+
meme: { base: 300, priority: 2, category: 'social' },
|
|
38
|
+
vote: { base: 7200, priority: 1, category: 'special' },
|
|
39
|
+
daily: { base: 86400, priority: 1, category: 'special' },
|
|
40
|
+
weekly: { base: 604800, priority: 1, category: 'special' },
|
|
41
|
+
monthly: { base: 2592000, priority: 1, category: 'special' },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ── EMA Calculator ────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Exponential Moving Average for cooldown prediction.
|
|
47
|
+
* Gives more weight to recent observations.
|
|
48
|
+
*/
|
|
49
|
+
class EMACalculator {
|
|
50
|
+
constructor(alpha = 0.3) {
|
|
51
|
+
this.alpha = alpha; // Smoothing factor (0-1)
|
|
52
|
+
this.ema = null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
update(value) {
|
|
56
|
+
if (this.ema === null) {
|
|
57
|
+
this.ema = value;
|
|
58
|
+
} else {
|
|
59
|
+
this.ema = this.alpha * value + (1 - this.alpha) * this.ema;
|
|
60
|
+
}
|
|
61
|
+
return this.ema;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getPrediction() {
|
|
65
|
+
return this.ema;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
reset() {
|
|
69
|
+
this.ema = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Cooldown Tracker ──────────────────────────────────────────
|
|
74
|
+
class CooldownTracker {
|
|
75
|
+
constructor(userId, redis = null, clusterEnabled = false) {
|
|
76
|
+
this.userId = userId;
|
|
77
|
+
this.redis = redis;
|
|
78
|
+
this.clusterEnabled = clusterEnabled;
|
|
79
|
+
|
|
80
|
+
// Local caches
|
|
81
|
+
this.cooldowns = new Map(); // command -> readyAt (timestamp)
|
|
82
|
+
this.emaTrackers = new Map(); // command -> EMACalculator
|
|
83
|
+
this.historyCache = new LRUCache(50); // Last 50 cooldown observations per command
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Record a cooldown for a command.
|
|
88
|
+
* @param {string} command - Command name
|
|
89
|
+
* @param {number} cooldownSec - Observed cooldown in seconds
|
|
90
|
+
*/
|
|
91
|
+
recordCooldown(command, cooldownSec) {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
const readyAt = now + (cooldownSec * 1000);
|
|
94
|
+
|
|
95
|
+
// Store in local map
|
|
96
|
+
this.cooldowns.set(command, readyAt);
|
|
97
|
+
|
|
98
|
+
// Update EMA prediction
|
|
99
|
+
if (!this.emaTrackers.has(command)) {
|
|
100
|
+
this.emaTrackers.set(command, new EMACalculator(0.3));
|
|
101
|
+
}
|
|
102
|
+
const ema = this.emaTrackers.get(command);
|
|
103
|
+
ema.update(cooldownSec);
|
|
104
|
+
|
|
105
|
+
// Store in Redis for cluster mode
|
|
106
|
+
if (this.redis && this.clusterEnabled) {
|
|
107
|
+
const key = `dkg:cd:${this.userId}:${command}`;
|
|
108
|
+
this.redis.setex(key, cooldownSec + 60, JSON.stringify({
|
|
109
|
+
readyAt,
|
|
110
|
+
ema: ema.getPrediction(),
|
|
111
|
+
recorded: now,
|
|
112
|
+
})).catch(() => {});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Cache history
|
|
116
|
+
const history = this.historyCache.get(command) || [];
|
|
117
|
+
history.push({ cooldownSec, timestamp: now });
|
|
118
|
+
if (history.length > 50) history.shift();
|
|
119
|
+
this.historyCache.set(command, history);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if a command is ready.
|
|
124
|
+
* @param {string} command - Command name
|
|
125
|
+
* @returns {Object} - { ready: boolean, waitMs: number, predicted: number|null }
|
|
126
|
+
*/
|
|
127
|
+
isReady(command) {
|
|
128
|
+
const readyAt = this.cooldowns.get(command);
|
|
129
|
+
if (!readyAt) {
|
|
130
|
+
return { ready: true, waitMs: 0, predicted: null };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
const waitMs = Math.max(0, readyAt - now);
|
|
135
|
+
|
|
136
|
+
// Add smart buffer
|
|
137
|
+
const config = COMMAND_CONFIG[command];
|
|
138
|
+
if (config) {
|
|
139
|
+
const buffered = calcSmartCooldown(waitMs / 1000, { addBuffer: true, addVariance: false });
|
|
140
|
+
return {
|
|
141
|
+
ready: waitMs <= 0,
|
|
142
|
+
waitMs: waitMs > 0 ? buffered * 1000 : 0,
|
|
143
|
+
predicted: this.emaTrackers.get(command)?.getPrediction() || null,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
ready: waitMs <= 0,
|
|
149
|
+
waitMs,
|
|
150
|
+
predicted: this.emaTrackers.get(command)?.getPrediction() || null,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get predicted cooldown for a command.
|
|
156
|
+
* @param {string} command - Command name
|
|
157
|
+
* @returns {number|null} - Predicted cooldown in seconds
|
|
158
|
+
*/
|
|
159
|
+
getPredictedCooldown(command) {
|
|
160
|
+
return this.emaTrackers.get(command)?.getPrediction() || null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get optimal command order based on cooldowns.
|
|
165
|
+
* @param {string[]} commands - List of commands to consider
|
|
166
|
+
* @returns {Array} - Sorted commands with timing info
|
|
167
|
+
*/
|
|
168
|
+
getOptimalOrder(commands) {
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
const order = commands.map(cmd => {
|
|
171
|
+
const config = COMMAND_CONFIG[cmd] || { priority: 5 };
|
|
172
|
+
const status = this.isReady(cmd);
|
|
173
|
+
return {
|
|
174
|
+
command: cmd,
|
|
175
|
+
ready: status.ready,
|
|
176
|
+
waitMs: status.waitMs,
|
|
177
|
+
priority: config.priority,
|
|
178
|
+
category: config.category,
|
|
179
|
+
predicted: status.predicted,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Sort: ready commands first (by priority), then by wait time
|
|
184
|
+
order.sort((a, b) => {
|
|
185
|
+
if (a.ready && !b.ready) return -1;
|
|
186
|
+
if (!a.ready && b.ready) return 1;
|
|
187
|
+
if (a.ready && b.ready) return a.priority - b.priority;
|
|
188
|
+
return a.waitMs - b.waitMs;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return order;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get all commands that will be ready within a time window.
|
|
196
|
+
* @param {number} windowMs - Time window in milliseconds
|
|
197
|
+
* @returns {Array} - Commands ready within window
|
|
198
|
+
*/
|
|
199
|
+
getReadyInWindow(windowMs) {
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
const ready = [];
|
|
202
|
+
|
|
203
|
+
for (const [command, readyAt] of this.cooldowns.entries()) {
|
|
204
|
+
const waitMs = readyAt - now;
|
|
205
|
+
if (waitMs <= windowMs) {
|
|
206
|
+
ready.push({
|
|
207
|
+
command,
|
|
208
|
+
waitMs: Math.max(0, waitMs),
|
|
209
|
+
readyAt,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
ready.sort((a, b) => a.waitMs - b.waitMs);
|
|
215
|
+
return ready;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Clear cooldown for a command.
|
|
220
|
+
* @param {string} command - Command name
|
|
221
|
+
*/
|
|
222
|
+
clearCooldown(command) {
|
|
223
|
+
this.cooldowns.delete(command);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Clear all cooldowns.
|
|
228
|
+
*/
|
|
229
|
+
clearAll() {
|
|
230
|
+
this.cooldowns.clear();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Command Scheduler (Priority Queue) ────────────────────────
|
|
235
|
+
class CommandScheduler {
|
|
236
|
+
constructor(tracker) {
|
|
237
|
+
this.tracker = tracker;
|
|
238
|
+
this.queue = new MinHeap((a, b) => a.executeAt - b.executeAt);
|
|
239
|
+
this.pending = new Map(); // command -> node reference
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Schedule a command for execution.
|
|
244
|
+
* @param {string} command - Command name
|
|
245
|
+
* @param {Function} executeFn - Function to execute
|
|
246
|
+
* @param {number} executeAt - Timestamp to execute at
|
|
247
|
+
*/
|
|
248
|
+
schedule(command, executeFn, executeAt) {
|
|
249
|
+
// Remove existing scheduled command if any
|
|
250
|
+
this.unschedule(command);
|
|
251
|
+
|
|
252
|
+
const node = {
|
|
253
|
+
command,
|
|
254
|
+
executeFn,
|
|
255
|
+
executeAt,
|
|
256
|
+
added: Date.now(),
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
this.queue.push(node);
|
|
260
|
+
this.pending.set(command, node);
|
|
261
|
+
|
|
262
|
+
return node;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Cancel a scheduled command.
|
|
267
|
+
* @param {string} command - Command name
|
|
268
|
+
*/
|
|
269
|
+
unschedule(command) {
|
|
270
|
+
const node = this.pending.get(command);
|
|
271
|
+
if (node) {
|
|
272
|
+
this.queue.remove(node);
|
|
273
|
+
this.pending.delete(command);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get next command to execute.
|
|
279
|
+
* @returns {Object|null} - Next command node or null
|
|
280
|
+
*/
|
|
281
|
+
peekNext() {
|
|
282
|
+
return this.queue.peek();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Execute next command if it's time.
|
|
287
|
+
* @returns {Object|null} - Executed command node or null
|
|
288
|
+
*/
|
|
289
|
+
async executeNext() {
|
|
290
|
+
const node = this.queue.peek();
|
|
291
|
+
if (!node) return null;
|
|
292
|
+
|
|
293
|
+
const now = Date.now();
|
|
294
|
+
if (node.executeAt <= now) {
|
|
295
|
+
this.queue.pop();
|
|
296
|
+
this.pending.delete(node.command);
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
await node.executeFn();
|
|
300
|
+
return node;
|
|
301
|
+
} catch (e) {
|
|
302
|
+
console.error(`[scheduler] Error executing ${node.command}:`, e);
|
|
303
|
+
throw e;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get queue size.
|
|
312
|
+
*/
|
|
313
|
+
size() {
|
|
314
|
+
return this.queue.size();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Factory Function ──────────────────────────────────────────
|
|
319
|
+
/**
|
|
320
|
+
* Create a cooldown manager for a user.
|
|
321
|
+
* @param {string} userId - User ID
|
|
322
|
+
* @param {Object} options
|
|
323
|
+
* @param {Object} options.redis - Redis client (optional)
|
|
324
|
+
* @param {boolean} options.clusterEnabled - Cluster mode flag
|
|
325
|
+
* @returns {Object} - { tracker, scheduler, COMMAND_CONFIG }
|
|
326
|
+
*/
|
|
327
|
+
function createCooldownManager(userId, options = {}) {
|
|
328
|
+
const { redis = null, clusterEnabled = false } = options;
|
|
329
|
+
|
|
330
|
+
const tracker = new CooldownTracker(userId, redis, clusterEnabled);
|
|
331
|
+
const scheduler = new CommandScheduler(tracker);
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
tracker,
|
|
335
|
+
scheduler,
|
|
336
|
+
COMMAND_CONFIG,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Exports ───────────────────────────────────────────────────
|
|
341
|
+
module.exports = {
|
|
342
|
+
EMACalculator,
|
|
343
|
+
CooldownTracker,
|
|
344
|
+
CommandScheduler,
|
|
345
|
+
createCooldownManager,
|
|
346
|
+
COMMAND_CONFIG,
|
|
347
|
+
};
|
package/lib/grinder.js
CHANGED
|
@@ -316,37 +316,48 @@ function renderDashboard() {
|
|
|
316
316
|
const tw = Math.min(process.stdout.columns || 80, 78);
|
|
317
317
|
const thinBar = c.dim + '─'.repeat(tw) + c.reset;
|
|
318
318
|
const bar = rgb(139, 92, 246) + c.bold + '━'.repeat(tw) + c.reset;
|
|
319
|
+
const doubleBar = rgb(139, 92, 246) + '╔' + '═'.repeat(tw - 2) + '╗' + c.reset;
|
|
320
|
+
const doubleBarBot = rgb(139, 92, 246) + '╚' + '═'.repeat(tw - 2) + '╝' + c.reset;
|
|
319
321
|
|
|
320
322
|
// Header with dynamic version, command count, and status
|
|
321
|
-
lines.push(
|
|
323
|
+
lines.push(doubleBar);
|
|
322
324
|
const cmdCount = AccountWorker.COMMAND_MAP.length;
|
|
323
325
|
const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
|
|
324
326
|
const mode = CLUSTER_ENABLED ? `${rgb(34, 211, 238)}Cluster${c.reset}` : `${c.dim}Standalone${c.reset}`;
|
|
327
|
+
|
|
328
|
+
// Animated spinner based on time
|
|
329
|
+
const spinners = ['◐', '◓', '◑', '◒'];
|
|
330
|
+
const spinner = spinners[Math.floor(Date.now() / 250) % 4];
|
|
331
|
+
const animatedSpinner = `${rgb(52, 211, 153)}${spinner}${c.reset}`;
|
|
332
|
+
|
|
325
333
|
lines.push(
|
|
326
334
|
` ${rgb(139, 92, 246)}${c.bold}DankGrinder${c.reset} ${c.dim}v${PKG_VERSION}${c.reset}` +
|
|
327
335
|
` ${c.dim}·${c.reset} ${c.white}${cmdCount} Cmds${c.reset}` +
|
|
328
336
|
` ${c.dim}·${c.reset} ${mode}` +
|
|
329
|
-
` ${c.dim}·${c.reset} ${rgb(52, 211, 153)}${activeCount}${c.reset}${c.dim}/${c.reset}${c.white}${workers.length}${c.reset} ${c.dim}Live${c.reset}`
|
|
337
|
+
` ${c.dim}·${c.reset} ${animatedSpinner} ${rgb(52, 211, 153)}${activeCount}${c.reset}${c.dim}/${c.reset}${c.white}${workers.length}${c.reset} ${c.dim}Live${c.reset}`
|
|
330
338
|
);
|
|
331
339
|
|
|
332
|
-
// Stats row
|
|
333
|
-
const liveIcon = rgb(52, 211, 153) + '
|
|
340
|
+
// Stats row with enhanced visual indicators
|
|
341
|
+
const liveIcon = rgb(52, 211, 153) + '●' + c.reset;
|
|
334
342
|
const balStr = `${rgb(192, 132, 252)}${c.bold}⏣ ${formatCoins(totalBalance)}${c.reset}`;
|
|
335
|
-
const earnStr = `${rgb(52, 211, 153)}
|
|
343
|
+
const earnStr = `${rgb(52, 211, 153)}▲ ${formatCoins(totalCoins)}${c.reset}`;
|
|
336
344
|
// Coins/hour rate
|
|
337
345
|
const elapsedHrs = (Date.now() - startTime) / 3_600_000;
|
|
338
346
|
const coinsPerHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
|
|
339
347
|
const rateLabel = `${rgb(52, 211, 153)}${formatCoins(coinsPerHr)}/h${c.reset}`;
|
|
340
348
|
const cmdStr = `${rgb(96, 165, 250)}${totalCommands}${c.reset}${c.dim} cmds${c.reset}`;
|
|
341
349
|
const rateStr = successRate >= 95
|
|
342
|
-
? `${rgb(52, 211, 153)}${successRate}%${c.reset}`
|
|
343
|
-
: successRate >= 80 ? `${rgb(251, 191, 36)}${successRate}%${c.reset}`
|
|
344
|
-
: `${rgb(239, 68, 68)}${successRate}%${c.reset}`;
|
|
345
|
-
const upStr = `${rgb(251, 191, 36)}
|
|
346
|
-
// Memory usage (RSS in MB)
|
|
350
|
+
? `${rgb(52, 211, 153)}● ${successRate}%${c.reset}`
|
|
351
|
+
: successRate >= 80 ? `${rgb(251, 191, 36)}● ${successRate}%${c.reset}`
|
|
352
|
+
: `${rgb(239, 68, 68)}● ${successRate}%${c.reset}`;
|
|
353
|
+
const upStr = `${rgb(251, 191, 36)}◷ ${formatUptime()}${c.reset}`;
|
|
354
|
+
// Memory usage (RSS in MB) with bar indicator
|
|
347
355
|
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
356
|
+
const memPct = Math.min(100, (memMB / 1024) * 100);
|
|
357
|
+
const memBarWidth = Math.floor((memPct / 100) * 10);
|
|
358
|
+
const memBar = rgb(52, 211, 153) + '▅'.repeat(memBarWidth) + c.dim + '▅'.repeat(10 - memBarWidth) + c.reset;
|
|
348
359
|
const memColor = memMB > 900 ? rgb(239, 68, 68) : memMB > 600 ? rgb(251, 191, 36) : rgb(52, 211, 153);
|
|
349
|
-
const memStr = `${memColor}${memMB}MB${c.reset}`;
|
|
360
|
+
const memStr = `${memColor}${memMB}MB${c.reset} ${memBar}`;
|
|
350
361
|
// Commands/minute from SlidingWindowCounter
|
|
351
362
|
const cpmVal = globalCmdRate.getRate().toFixed(1);
|
|
352
363
|
const cpmStr = `${rgb(34, 211, 238)}${cpmVal}${c.reset}${c.dim}/m${c.reset}`;
|
|
@@ -394,9 +405,16 @@ function renderDashboard() {
|
|
|
394
405
|
const bal = wk.stats.balance > 0
|
|
395
406
|
? `${rgb(251, 191, 36)}⏣${c.reset} ${c.white}${formatCoins(wk.stats.balance).padStart(7)}${c.reset}`
|
|
396
407
|
: `${c.dim}⏣ -${c.reset}`;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
408
|
+
|
|
409
|
+
// Mini progress bar for earned coins (visual indicator of activity)
|
|
410
|
+
const earnedNum = wk.stats.coins || 0;
|
|
411
|
+
const earnedBarWidth = earnedNum > 0 ? Math.min(5, Math.max(1, Math.floor(Math.log10(earnedNum + 1)))) : 0;
|
|
412
|
+
const earnedBar = earnedNum > 0
|
|
413
|
+
? `${rgb(52, 211, 153)}${'▰'.repeat(earnedBarWidth)}${c.dim}${'▱'.repeat(5 - earnedBarWidth)}${c.reset}`
|
|
414
|
+
: `${c.dim}▱▱▱▱▱${c.reset}`;
|
|
415
|
+
const earned = earnedNum > 0
|
|
416
|
+
? `${rgb(52, 211, 153)}+${formatCoins(earnedNum)}${c.reset} ${earnedBar}`
|
|
417
|
+
: `${c.dim}+0${c.reset} ${earnedBar}`;
|
|
400
418
|
|
|
401
419
|
lines.push(` ${dot} ${name.padEnd(nameWidth + wk.color.length + c.bold.length + c.reset.length)} ${bal} ${earned} ${stateLabel}`);
|
|
402
420
|
};
|
|
@@ -451,7 +469,7 @@ function renderDashboard() {
|
|
|
451
469
|
}
|
|
452
470
|
}
|
|
453
471
|
|
|
454
|
-
lines.push(
|
|
472
|
+
lines.push(doubleBarBot);
|
|
455
473
|
|
|
456
474
|
// Absolute cursor home — always draw from row 1
|
|
457
475
|
process.stdout.write('\x1b[H');
|
|
@@ -764,6 +782,13 @@ class MinHeap {
|
|
|
764
782
|
// ══════════════════════════════════════════════════════════════
|
|
765
783
|
|
|
766
784
|
class AccountWorker {
|
|
785
|
+
static SMART_CD_FLOORS = Object.freeze({
|
|
786
|
+
'farm': 30,
|
|
787
|
+
'adventure': 300,
|
|
788
|
+
'stream': 600,
|
|
789
|
+
'work shift': 1800,
|
|
790
|
+
});
|
|
791
|
+
|
|
767
792
|
constructor(account, idx) {
|
|
768
793
|
this.account = account;
|
|
769
794
|
this.idx = idx;
|
|
@@ -1147,7 +1172,7 @@ class AccountWorker {
|
|
|
1147
1172
|
lastErr = e;
|
|
1148
1173
|
if (attempt < tries) {
|
|
1149
1174
|
this.log('warn', `Inventory attempt ${attempt}/${tries} failed (${e.message}). Retrying...`);
|
|
1150
|
-
await
|
|
1175
|
+
await new Promise((r) => setTimeout(r, 1500 + Math.floor(Math.random() * 1500)));
|
|
1151
1176
|
continue;
|
|
1152
1177
|
}
|
|
1153
1178
|
}
|
|
@@ -1667,7 +1692,7 @@ class AccountWorker {
|
|
|
1667
1692
|
{ key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 7 },
|
|
1668
1693
|
// Fast grinders — 10s CD
|
|
1669
1694
|
{ key: 'cmd_hl', cmd: 'hl', cdKey: 'cd_hl', defaultCd: 10, priority: 6 },
|
|
1670
|
-
|
|
1695
|
+
{ key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 30, priority: 4 },
|
|
1671
1696
|
{ key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 6 },
|
|
1672
1697
|
{ key: 'cmd_use', cmd: 'use', cdKey: 'cd_use', defaultCd: 10, priority: 2 },
|
|
1673
1698
|
// Medium grinders — 20-25s CD
|
|
@@ -1681,10 +1706,10 @@ class AccountWorker {
|
|
|
1681
1706
|
{ key: 'cmd_crime', cmd: 'crime', cdKey: 'cd_crime', defaultCd: 40, priority: 5 },
|
|
1682
1707
|
{ key: 'cmd_tidy', cmd: 'tidy', cdKey: 'cd_tidy', defaultCd: 40, priority: 2 },
|
|
1683
1708
|
// Interactive — response-driven CD (handler sets nextCooldownSec)
|
|
1684
|
-
|
|
1685
|
-
|
|
1709
|
+
{ key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 3 },
|
|
1710
|
+
{ key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 3 },
|
|
1686
1711
|
{ key: 'cmd_scratch', cmd: 'scratch', cdKey: 'cd_scratch', defaultCd: 10, priority: 3 },
|
|
1687
|
-
|
|
1712
|
+
{ key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
|
|
1688
1713
|
// Time-gated (run ASAP when available)
|
|
1689
1714
|
{ key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
|
|
1690
1715
|
{ key: 'cmd_weekly', cmd: 'weekly', cdKey: 'cd_weekly', defaultCd: 604800, priority: 10 },
|
|
@@ -2029,8 +2054,6 @@ class AccountWorker {
|
|
|
2029
2054
|
const microPause = Math.random() < 0.08 ? 1.5 + Math.random() * 3 : 0;
|
|
2030
2055
|
const totalWait = cd + jitterBase + microPause;
|
|
2031
2056
|
|
|
2032
|
-
await this.setCooldown(item.cmd, totalWait);
|
|
2033
|
-
|
|
2034
2057
|
const timeSinceLastCmd = now - (this.lastCommandRun || 0);
|
|
2035
2058
|
const gapBase = cd <= 5 ? 1500 : cd <= 20 ? 2000 : 2500;
|
|
2036
2059
|
const jitterGap = patternMod.minDelay + Math.random() * (patternMod.maxDelay - patternMod.minDelay);
|
|
@@ -2059,7 +2082,7 @@ class AccountWorker {
|
|
|
2059
2082
|
// Grace period for interactive (button-click) commands — Dank Memer
|
|
2060
2083
|
// needs time to process the interaction before accepting the next command.
|
|
2061
2084
|
// Without this, the next command gets "Hold Tight" errors.
|
|
2062
|
-
const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'scratch', 'adventure', 'stream', 'fish']);
|
|
2085
|
+
const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'scratch', 'adventure', 'stream', 'fish', 'farm', 'work shift']);
|
|
2063
2086
|
if (INTERACTIVE_CMDS.has(item.cmd)) {
|
|
2064
2087
|
await new Promise(r => setTimeout(r, 2500 + Math.random() * 1500));
|
|
2065
2088
|
}
|
|
@@ -2078,8 +2101,7 @@ class AccountWorker {
|
|
|
2078
2101
|
this.failStreak = 0;
|
|
2079
2102
|
}
|
|
2080
2103
|
|
|
2081
|
-
|
|
2082
|
-
await this.setCooldown(item.cmd, totalWait);
|
|
2104
|
+
this.lastCommandRun = Date.now();
|
|
2083
2105
|
|
|
2084
2106
|
// Exponential backoff: if too many consecutive failures, slow down
|
|
2085
2107
|
const backoffMultiplier = this.failStreak > 5 ? Math.min(this.failStreak - 4, 5) : 1;
|
|
@@ -2087,12 +2109,25 @@ class AccountWorker {
|
|
|
2087
2109
|
const MIN_FAIL_COOLDOWN = 5;
|
|
2088
2110
|
|
|
2089
2111
|
if (this.commandQueue && this.running && !shutdownCalled) {
|
|
2090
|
-
|
|
2112
|
+
const hasOverride = Number.isFinite(this._lastCooldownOverride) && this._lastCooldownOverride > 0;
|
|
2113
|
+
let effectiveWait = hasOverride ? this._lastCooldownOverride : totalWait;
|
|
2091
2114
|
this._lastCooldownOverride = null;
|
|
2115
|
+
|
|
2116
|
+
// Smart fallback floors for long/interactive commands when parser misses exact cooldown.
|
|
2117
|
+
if (!hasOverride) {
|
|
2118
|
+
const floor = AccountWorker.SMART_CD_FLOORS[item.cmd];
|
|
2119
|
+
if (Number.isFinite(floor) && floor > 0) {
|
|
2120
|
+
effectiveWait = Math.max(effectiveWait, floor);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2092
2124
|
if (earned <= 0 && !noFailCmds.includes(item.cmd) && effectiveWait < MIN_FAIL_COOLDOWN) {
|
|
2093
2125
|
effectiveWait = MIN_FAIL_COOLDOWN;
|
|
2094
2126
|
}
|
|
2095
|
-
|
|
2127
|
+
|
|
2128
|
+
const scheduledWaitSec = Math.max(1, effectiveWait * backoffMultiplier);
|
|
2129
|
+
await this.setCooldown(item.cmd, scheduledWaitSec);
|
|
2130
|
+
item.nextRunAt = Date.now() + scheduledWaitSec * 1000;
|
|
2096
2131
|
this.commandQueue.push(item);
|
|
2097
2132
|
}
|
|
2098
2133
|
|
|
@@ -2438,17 +2473,31 @@ async function start(apiKey, apiUrl) {
|
|
|
2438
2473
|
console.log('');
|
|
2439
2474
|
|
|
2440
2475
|
// Phase 1: Login all accounts (staggered to avoid 429s)
|
|
2441
|
-
const
|
|
2442
|
-
const
|
|
2476
|
+
const LOGIN_PROGRESS_EVERY = 5;
|
|
2477
|
+
const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '100'), 10);
|
|
2478
|
+
const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '300'), 10);
|
|
2479
|
+
const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 100;
|
|
2480
|
+
const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(LOGIN_GAP_MIN_MS, 300);
|
|
2481
|
+
|
|
2482
|
+
const randomLoginGap = () => {
|
|
2483
|
+
if (LOGIN_GAP_MAX_MS <= LOGIN_GAP_MIN_MS) return LOGIN_GAP_MIN_MS;
|
|
2484
|
+
return LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
|
|
2485
|
+
};
|
|
2486
|
+
|
|
2443
2487
|
for (let i = 0; i < accounts.length; i++) {
|
|
2444
2488
|
if (shutdownCalled) break;
|
|
2445
2489
|
const worker = new AccountWorker(accounts[i], i);
|
|
2446
2490
|
workers.push(worker);
|
|
2447
2491
|
workerMap.set(accounts[i].id, worker);
|
|
2448
2492
|
await worker.start();
|
|
2449
|
-
if (
|
|
2450
|
-
|
|
2451
|
-
|
|
2493
|
+
if (i + 1 < accounts.length) {
|
|
2494
|
+
const gapMs = randomLoginGap();
|
|
2495
|
+
if ((i + 1) % LOGIN_PROGRESS_EVERY === 0) {
|
|
2496
|
+
log('info', `${c.dim}Logged in ${i + 1}/${accounts.length}, next account in ${gapMs}ms...${c.reset}`);
|
|
2497
|
+
}
|
|
2498
|
+
await new Promise(r => setTimeout(r, gapMs));
|
|
2499
|
+
}
|
|
2500
|
+
if ((i + 1) % LOGIN_PROGRESS_EVERY === 0) {
|
|
2452
2501
|
hintGC();
|
|
2453
2502
|
}
|
|
2454
2503
|
}
|