dankgrinder 4.0.0 → 4.1.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/README.md +25 -16
- package/bin/dankgrinder.js +18 -28
- package/lib/commands/adventure.js +502 -0
- package/lib/commands/beg.js +45 -0
- package/lib/commands/blackjack.js +85 -0
- package/lib/commands/crime.js +94 -0
- package/lib/commands/deposit.js +46 -0
- package/lib/commands/dig.js +82 -0
- package/lib/commands/fish.js +615 -0
- package/lib/commands/fishVision.js +141 -0
- package/lib/commands/gamble.js +96 -0
- package/lib/commands/generic.js +181 -0
- package/lib/commands/highlow.js +112 -0
- package/lib/commands/hunt.js +85 -0
- package/lib/commands/index.js +59 -0
- package/lib/commands/postmemes.js +148 -0
- package/lib/commands/profile.js +99 -0
- package/lib/commands/scratch.js +83 -0
- package/lib/commands/search.js +102 -0
- package/lib/commands/shop.js +262 -0
- package/lib/commands/trivia.js +146 -0
- package/lib/commands/utils.js +287 -0
- package/lib/commands/work.js +400 -0
- package/lib/grinder.js +560 -656
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# DankGrinder CLI
|
|
2
2
|
|
|
3
|
-
Dank Memer automation engine —
|
|
3
|
+
Dank Memer automation engine — multi-account grinding with live TUI dashboard.
|
|
4
4
|
|
|
5
5
|
## Installation & Usage
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
# Run directly (no install needed)
|
|
9
|
-
npx dankgrinder --key dkg_your_api_key
|
|
9
|
+
npx dankgrinder --key dkg_your_api_key --url https://your-dashboard.com
|
|
10
10
|
|
|
11
11
|
# Or install globally
|
|
12
12
|
npm install -g dankgrinder
|
|
13
|
-
dankgrinder --key dkg_your_api_key
|
|
13
|
+
dankgrinder --key dkg_your_api_key --url https://your-dashboard.com
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
## Options
|
|
@@ -18,30 +18,39 @@ dankgrinder --key dkg_your_api_key
|
|
|
18
18
|
| Flag | Description | Default |
|
|
19
19
|
|------|-------------|---------|
|
|
20
20
|
| `--key <key>` | Your DankGrinder API key (required) | — |
|
|
21
|
-
| `--url <url>` | API server URL |
|
|
21
|
+
| `--url <url>` | API server URL (required) | — |
|
|
22
22
|
| `--help` | Show help | — |
|
|
23
23
|
| `--version` | Show version | — |
|
|
24
24
|
|
|
25
25
|
## Setup
|
|
26
26
|
|
|
27
27
|
1. Sign up at your DankGrinder dashboard
|
|
28
|
-
2. Go to **
|
|
29
|
-
3. Enable the commands you want to automate
|
|
28
|
+
2. Go to **Accounts** → add your Discord account(s) with token and channel ID
|
|
29
|
+
3. Enable the commands you want to automate per account
|
|
30
30
|
4. Go to **API Keys** → create a new key
|
|
31
|
-
5. Run: `npx dankgrinder --key dkg_your_key`
|
|
31
|
+
5. Run: `npx dankgrinder --key dkg_your_key --url https://your-dashboard.com`
|
|
32
32
|
|
|
33
|
-
## Supported Commands
|
|
33
|
+
## Supported Commands (30)
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
| Category | Commands |
|
|
36
|
+
|----------|----------|
|
|
37
|
+
| **Grinding** | hunt, dig, fish, beg, search, crime, postmemes |
|
|
38
|
+
| **Games** | highlow, blackjack, coinflip, roulette, slots, snakeeyes, trivia, scratch |
|
|
39
|
+
| **Economy** | daily, weekly, monthly, work shift, stream, adventure, deposit |
|
|
40
|
+
| **Utility** | farm, tidy, use, drops, alert |
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- Multi-account support with per-account config
|
|
45
|
+
- Redis-backed cooldowns per command per account
|
|
46
|
+
- Live TUI dashboard with real-time stats
|
|
47
|
+
- Auto-buy tools (shovel, fishing pole, rifle) when missing
|
|
48
|
+
- Smart button/interaction handling for all mini-games
|
|
49
|
+
- Hold Tight detection with automatic cooldown management
|
|
42
50
|
|
|
43
51
|
## Requirements
|
|
44
52
|
|
|
45
53
|
- Node.js 18+
|
|
46
54
|
- A DankGrinder account with API key
|
|
47
|
-
- Discord token configured in dashboard
|
|
55
|
+
- Discord token(s) configured in dashboard
|
|
56
|
+
- Redis (optional, for persistent cooldowns via `REDIS_URL` env var)
|
package/bin/dankgrinder.js
CHANGED
|
@@ -6,30 +6,20 @@ const args = process.argv.slice(2);
|
|
|
6
6
|
|
|
7
7
|
if (args.includes('--help') || args.includes('-h')) {
|
|
8
8
|
console.log(`
|
|
9
|
-
\x1b[
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
\x1b[
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
\x1b[1mExamples:\x1b[0m
|
|
24
|
-
npx dankgrinder --key dkg_abc123def456
|
|
25
|
-
npx dankgrinder --key dkg_abc123def456 --url https://myserver.com
|
|
26
|
-
|
|
27
|
-
\x1b[1mSetup:\x1b[0m
|
|
28
|
-
1. Sign up at your DankGrinder dashboard
|
|
29
|
-
2. Go to the Accounts page and add your Discord accounts
|
|
30
|
-
3. Configure per-account commands and cooldowns
|
|
31
|
-
4. Generate an API key from Auth Tokens
|
|
32
|
-
5. Run this CLI — it spawns one worker per active account
|
|
9
|
+
\x1b[1m\x1b[35mDANK\x1b[36mGRINDER\x1b[0m \x1b[2mv4.0\x1b[0m
|
|
10
|
+
|
|
11
|
+
\x1b[1mUsage:\x1b[0m
|
|
12
|
+
npx dankgrinder --key <API_KEY> --url <DASHBOARD_URL>
|
|
13
|
+
|
|
14
|
+
\x1b[1mOptions:\x1b[0m
|
|
15
|
+
--key <key> API key from dashboard (or env DANKGRINDER_KEY)
|
|
16
|
+
--url <url> Dashboard URL (or env DANKGRINDER_URL)
|
|
17
|
+
--help, -h Show this help
|
|
18
|
+
--version, -v Show version
|
|
19
|
+
|
|
20
|
+
\x1b[1mExamples:\x1b[0m
|
|
21
|
+
npx dankgrinder --key dkg_abc123 --url https://myapp.up.railway.app
|
|
22
|
+
DANKGRINDER_KEY=dkg_abc123 DANKGRINDER_URL=https://... npx dankgrinder
|
|
33
23
|
`);
|
|
34
24
|
process.exit(0);
|
|
35
25
|
}
|
|
@@ -40,8 +30,8 @@ if (args.includes('--version') || args.includes('-v')) {
|
|
|
40
30
|
process.exit(0);
|
|
41
31
|
}
|
|
42
32
|
|
|
43
|
-
let apiKey = '';
|
|
44
|
-
let apiUrl = '
|
|
33
|
+
let apiKey = process.env.DANKGRINDER_KEY || '';
|
|
34
|
+
let apiUrl = process.env.DANKGRINDER_URL || '';
|
|
45
35
|
|
|
46
36
|
for (let i = 0; i < args.length; i++) {
|
|
47
37
|
if (args[i] === '--key' && args[i + 1]) apiKey = args[i + 1];
|
|
@@ -51,9 +41,9 @@ for (let i = 0; i < args.length; i++) {
|
|
|
51
41
|
if (!apiKey) {
|
|
52
42
|
console.error('\x1b[31m✗ Missing API key.\x1b[0m');
|
|
53
43
|
console.error('');
|
|
54
|
-
console.error(' Usage: npx dankgrinder --key <YOUR_API_KEY>');
|
|
44
|
+
console.error(' Usage: npx dankgrinder --key <YOUR_API_KEY> --url <DASHBOARD_URL>');
|
|
55
45
|
console.error('');
|
|
56
|
-
console.error('
|
|
46
|
+
console.error(' Or set env vars: DANKGRINDER_KEY, DANKGRINDER_URL');
|
|
57
47
|
console.error(' Run \x1b[36mnpx dankgrinder --help\x1b[0m for more info.');
|
|
58
48
|
process.exit(1);
|
|
59
49
|
}
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adventure command handler.
|
|
3
|
+
*
|
|
4
|
+
* Confirmed flow from live debugging:
|
|
5
|
+
* ─────────────────────────────────────────────────────────────
|
|
6
|
+
* 1. "pls adventure"
|
|
7
|
+
* → SELECT_MENU: adventure types (space, west, halloween, museum, brazil, etc.)
|
|
8
|
+
* → "Start" BUTTON (customId: adventure-backpack:ID:TYPE, style=SUCCESS)
|
|
9
|
+
* → If no ticket: Start is disabled
|
|
10
|
+
*
|
|
11
|
+
* 2. Click Start
|
|
12
|
+
* → Equip screen: item grid buttons (null labels, emojis) + "Start" + "Equip All" + "Cancel"
|
|
13
|
+
* → Click "Equip All" then "Start"
|
|
14
|
+
*
|
|
15
|
+
* 3. Adventure rounds (5 interactions shown in footer like "🚀 - ⦾ - ⦾ - ⦾ - ⦾"):
|
|
16
|
+
* Each round has:
|
|
17
|
+
* • Embed with event description + footer showing progress
|
|
18
|
+
* • Backpack btn (emoji=Backpack, customId=adventure-progress:ID) — view-only
|
|
19
|
+
* • ArrowRightui btn (customId=adventure-next:ID) — advance to next interaction
|
|
20
|
+
* • Sometimes: choice buttons (customId=adventure-option:ID:N:M) like "Reach for it" / "Flee"
|
|
21
|
+
*
|
|
22
|
+
* When choices are present → ArrowRightui is DISABLED until a choice is made.
|
|
23
|
+
* After choosing → ArrowRightui becomes enabled. Click it to advance.
|
|
24
|
+
* When no choices → ArrowRightui is enabled. Click it to advance directly.
|
|
25
|
+
*
|
|
26
|
+
* 4. After last interaction:
|
|
27
|
+
* → "Adventure Progress" embed with Backpack, Rewards, Interactions fields
|
|
28
|
+
* → No clickable buttons → adventure is done
|
|
29
|
+
*
|
|
30
|
+
* CRITICAL: safeClickButton()/clickButton() returns an interaction object,
|
|
31
|
+
* NOT the updated message. Must re-fetch via channel.messages.fetch(msg.id)
|
|
32
|
+
* to see the updated state after each click.
|
|
33
|
+
* ─────────────────────────────────────────────────────────────
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const {
|
|
37
|
+
LOG, c, sleep, humanDelay, getFullText, parseCoins, parseBalance,
|
|
38
|
+
getAllButtons, getAllSelectMenus, findButton, findSelectMenuOption,
|
|
39
|
+
safeClickButton, isHoldTight, logMsg, dumpMessage, needsItem,
|
|
40
|
+
} = require('./utils');
|
|
41
|
+
const { buyItem } = require('./shop');
|
|
42
|
+
|
|
43
|
+
// ── Adventure type rotation (cycle through all types each run) ────
|
|
44
|
+
let lastAdventureIndex = -1;
|
|
45
|
+
|
|
46
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/** Re-fetch a message to get its current state after an interaction edit */
|
|
49
|
+
async function refetchMsg(channel, msgId) {
|
|
50
|
+
try {
|
|
51
|
+
return await channel.messages.fetch(msgId);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
LOG.error(`[adventure] Re-fetch failed: ${e.message}`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Click a button on msg, then re-fetch msg to get updated state */
|
|
59
|
+
async function clickAndRefetch(channel, msg, btn) {
|
|
60
|
+
try {
|
|
61
|
+
await safeClickButton(msg, btn);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
LOG.error(`[adventure] Click error: ${e.message}`);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
// Dank Memer edits the message after a button click
|
|
67
|
+
await sleep(500);
|
|
68
|
+
return await refetchMsg(channel, msg.id);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Parse footer progress like "🚀 - ⦾ - ⦾ - ⦾ - ⦾" → { current: 1, total: 5 } */
|
|
72
|
+
function parseProgress(msg) {
|
|
73
|
+
const footer = msg.embeds?.[0]?.footer?.text || '';
|
|
74
|
+
// Count total markers and find the rocket position
|
|
75
|
+
const markers = footer.split(' - ');
|
|
76
|
+
if (markers.length < 2) return null;
|
|
77
|
+
const current = markers.findIndex(m => m.includes('🚀')) + 1;
|
|
78
|
+
return { current, total: markers.length };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Get all choice buttons (adventure-option) that are clickable */
|
|
82
|
+
function getChoiceButtons(msg) {
|
|
83
|
+
return getAllButtons(msg).filter(b =>
|
|
84
|
+
!b.disabled && (b.customId || '').includes('adventure-option')
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Get the "next" arrow button */
|
|
89
|
+
function getNextButton(msg) {
|
|
90
|
+
return getAllButtons(msg).find(b =>
|
|
91
|
+
(b.customId || '').includes('adventure-next')
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Check if msg is the final "Adventure Progress" summary */
|
|
96
|
+
function isAdventureDone(msg) {
|
|
97
|
+
const text = getFullText(msg).toLowerCase();
|
|
98
|
+
const title = msg.embeds?.[0]?.title || '';
|
|
99
|
+
if (title === 'Adventure Progress') return true;
|
|
100
|
+
// Also done if no clickable buttons at all
|
|
101
|
+
const clickable = getAllButtons(msg).filter(b => !b.disabled);
|
|
102
|
+
if (clickable.length === 0) return true;
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Safe choices: prefer non-destructive options ─────────────────
|
|
107
|
+
const SAFE_KEYWORDS = ['flee', 'run', 'hide', 'avoid', 'ignore', 'leave', 'walk away', 'back away', 'retreat', 'skip'];
|
|
108
|
+
const RISKY_KEYWORDS = ['reach', 'grab', 'fight', 'attack', 'steal', 'open', 'touch', 'eat', 'drink'];
|
|
109
|
+
|
|
110
|
+
function pickSafeChoice(choices) {
|
|
111
|
+
if (choices.length === 0) return null;
|
|
112
|
+
if (choices.length === 1) return choices[0];
|
|
113
|
+
|
|
114
|
+
// Check labels for safe/risky keywords
|
|
115
|
+
const labels = choices.map(b => (b.label || '').toLowerCase());
|
|
116
|
+
|
|
117
|
+
// Prefer safe keywords
|
|
118
|
+
for (let i = 0; i < labels.length; i++) {
|
|
119
|
+
for (const kw of SAFE_KEYWORDS) {
|
|
120
|
+
if (labels[i].includes(kw)) {
|
|
121
|
+
LOG.info(`[adventure] Picking safe option: "${choices[i].label}" (matched: ${kw})`);
|
|
122
|
+
return choices[i];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Avoid risky keywords
|
|
128
|
+
const nonRisky = choices.filter((b, i) => {
|
|
129
|
+
return !RISKY_KEYWORDS.some(kw => labels[i].includes(kw));
|
|
130
|
+
});
|
|
131
|
+
if (nonRisky.length > 0) {
|
|
132
|
+
const pick = nonRisky[Math.floor(Math.random() * nonRisky.length)];
|
|
133
|
+
LOG.info(`[adventure] Picking non-risky option: "${pick.label}"`);
|
|
134
|
+
return pick;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fallback: random
|
|
138
|
+
const pick = choices[Math.floor(Math.random() * choices.length)];
|
|
139
|
+
LOG.info(`[adventure] Picking random option: "${pick.label}"`);
|
|
140
|
+
return pick;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Play through all adventure rounds ────────────────────────────
|
|
144
|
+
async function playAdventureRounds(channel, msg) {
|
|
145
|
+
let current = msg;
|
|
146
|
+
let interactions = 0;
|
|
147
|
+
const MAX_INTERACTIONS = 30;
|
|
148
|
+
|
|
149
|
+
while (interactions < MAX_INTERACTIONS) {
|
|
150
|
+
interactions++;
|
|
151
|
+
|
|
152
|
+
const progress = parseProgress(current);
|
|
153
|
+
const fullText = getFullText(current);
|
|
154
|
+
const choices = getChoiceButtons(current);
|
|
155
|
+
const nextBtn = getNextButton(current);
|
|
156
|
+
const done = isAdventureDone(current);
|
|
157
|
+
|
|
158
|
+
if (progress) {
|
|
159
|
+
LOG.info(`[adventure] Interaction ${progress.current}/${progress.total}: "${fullText.substring(0, 80).trim()}"`);
|
|
160
|
+
} else {
|
|
161
|
+
LOG.info(`[adventure] Step ${interactions}: "${fullText.substring(0, 80).trim()}"`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (done) {
|
|
165
|
+
LOG.success(`[adventure] Adventure finished after ${interactions} steps`);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── If there are choices, pick one first ─────────────────
|
|
170
|
+
if (choices.length > 0) {
|
|
171
|
+
LOG.info(`[adventure] ${choices.length} choices: [${choices.map(b => `"${b.label}"`).join(', ')}]`);
|
|
172
|
+
const choice = pickSafeChoice(choices);
|
|
173
|
+
if (choice) {
|
|
174
|
+
LOG.info(`[adventure] → Choosing: "${choice.label}"`);
|
|
175
|
+
await sleep(200);
|
|
176
|
+
const afterChoice = await clickAndRefetch(channel, current, choice);
|
|
177
|
+
if (afterChoice) {
|
|
178
|
+
current = afterChoice;
|
|
179
|
+
logMsg(current, `adv-choice-${interactions}`);
|
|
180
|
+
|
|
181
|
+
// Check if adventure ended after this choice
|
|
182
|
+
if (isAdventureDone(current)) {
|
|
183
|
+
LOG.success(`[adventure] Adventure ended after choice at step ${interactions}`);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
LOG.warn(`[adventure] No response after choice click`);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Click "Next" arrow to advance ────────────────────────
|
|
194
|
+
const nextBtnNow = getNextButton(current);
|
|
195
|
+
if (nextBtnNow && !nextBtnNow.disabled) {
|
|
196
|
+
LOG.debug(`[adventure] Clicking Next arrow...`);
|
|
197
|
+
await sleep(200);
|
|
198
|
+
const afterNext = await clickAndRefetch(channel, current, nextBtnNow);
|
|
199
|
+
if (afterNext) {
|
|
200
|
+
current = afterNext;
|
|
201
|
+
logMsg(current, `adv-round-${interactions}`);
|
|
202
|
+
} else {
|
|
203
|
+
LOG.warn(`[adventure] No response after Next click`);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
} else if (nextBtnNow && nextBtnNow.disabled) {
|
|
207
|
+
// Next is disabled but no choices found — might be loading
|
|
208
|
+
LOG.debug(`[adventure] Next disabled, no choices — waiting...`);
|
|
209
|
+
await sleep(600);
|
|
210
|
+
const refreshed = await refetchMsg(channel, current.id);
|
|
211
|
+
if (refreshed) {
|
|
212
|
+
current = refreshed;
|
|
213
|
+
// Don't increment interactions, just retry
|
|
214
|
+
interactions--;
|
|
215
|
+
} else {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
// No next button at all
|
|
220
|
+
LOG.debug(`[adventure] No Next button — checking if done`);
|
|
221
|
+
if (isAdventureDone(current)) break;
|
|
222
|
+
// Wait and re-fetch as Dank Memer might still be editing
|
|
223
|
+
await sleep(1500);
|
|
224
|
+
const refreshed = await refetchMsg(channel, current.id);
|
|
225
|
+
if (refreshed) {
|
|
226
|
+
current = refreshed;
|
|
227
|
+
if (isAdventureDone(current)) break;
|
|
228
|
+
interactions--;
|
|
229
|
+
} else {
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Parse final results
|
|
236
|
+
const finalText = getFullText(current);
|
|
237
|
+
const coins = parseCoins(finalText);
|
|
238
|
+
|
|
239
|
+
// Parse rewards from Adventure Progress embed
|
|
240
|
+
let rewards = '-';
|
|
241
|
+
for (const e of current.embeds || []) {
|
|
242
|
+
for (const f of e.fields || []) {
|
|
243
|
+
if (f.name === 'Rewards') rewards = f.value;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
LOG.info(`[adventure] Rewards: ${rewards}`);
|
|
247
|
+
|
|
248
|
+
return { text: finalText, coins, interactions, rewards, finalMsg: current };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Main: runAdventure ───────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {object} opts
|
|
255
|
+
* @param {object} opts.channel - Discord channel
|
|
256
|
+
* @param {function} opts.waitForDankMemer - Waits for Dank Memer response
|
|
257
|
+
* @param {object} [opts.client] - Discord client (for modal handling in shop)
|
|
258
|
+
* @returns {Promise<{result: string, coins: number, nextCooldownSec: number|null}>}
|
|
259
|
+
*/
|
|
260
|
+
async function runAdventure({ channel, waitForDankMemer, client }) {
|
|
261
|
+
LOG.cmd(`${c.white}${c.bold}pls adventure${c.reset}`);
|
|
262
|
+
|
|
263
|
+
// Step 1: Send the command
|
|
264
|
+
await channel.send('pls adventure');
|
|
265
|
+
let response = await waitForDankMemer(12000);
|
|
266
|
+
|
|
267
|
+
if (!response) {
|
|
268
|
+
LOG.warn('[adventure] No response from Dank Memer');
|
|
269
|
+
return { result: 'no response', coins: 0, nextCooldownSec: null };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Check for Hold Tight
|
|
273
|
+
if (isHoldTight(response)) {
|
|
274
|
+
LOG.warn('[adventure] Hold Tight — waiting 30s');
|
|
275
|
+
await sleep(30000);
|
|
276
|
+
return { result: 'hold tight', coins: 0, nextCooldownSec: 35 };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
logMsg(response, 'adventure-initial');
|
|
280
|
+
|
|
281
|
+
const text = getFullText(response);
|
|
282
|
+
const textLower = text.toLowerCase();
|
|
283
|
+
const allButtons = getAllButtons(response);
|
|
284
|
+
|
|
285
|
+
// ── 1) Check cooldown via Unix timestamp <t:UNIX:t> ────────
|
|
286
|
+
// Format: "You can start another adventure at <t:1774415487:t> (<t:1774415487:R>)"
|
|
287
|
+
let cooldownSec = 0;
|
|
288
|
+
const tsMatch = text.match(/<t:(\d+)(?::[tTdDfFR])?>/);
|
|
289
|
+
if (tsMatch) {
|
|
290
|
+
const unixTarget = parseInt(tsMatch[1]);
|
|
291
|
+
const nowUnix = Math.floor(Date.now() / 1000);
|
|
292
|
+
cooldownSec = Math.max(0, unixTarget - nowUnix);
|
|
293
|
+
if (cooldownSec > 0) {
|
|
294
|
+
LOG.warn(`[adventure] On cooldown — next at ${new Date(unixTarget * 1000).toLocaleTimeString()} (${cooldownSec}s / ${Math.ceil(cooldownSec / 60)}min)`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── 2) Check ticket need from button label ─────────────────
|
|
299
|
+
// Button: "Start (1 Adventure Ticket)" with emoji=AdventureTicket
|
|
300
|
+
// Footer: "You need adventure tickets to start the adventure!"
|
|
301
|
+
const startBtn = allButtons.find(b => (b.customId || '').includes('adventure-backpack'));
|
|
302
|
+
const startLabel = (startBtn?.label || '').toLowerCase();
|
|
303
|
+
const needsTicket = textLower.includes('you need adventure tickets') ||
|
|
304
|
+
(startLabel.includes('ticket') && startBtn?.disabled);
|
|
305
|
+
|
|
306
|
+
LOG.debug(`[adventure] startBtn="${startBtn?.label}" disabled=${startBtn?.disabled}, needsTicket=${needsTicket}, cooldown=${cooldownSec}s`);
|
|
307
|
+
|
|
308
|
+
// ── 3) If on cooldown, return with exact seconds ───────────
|
|
309
|
+
if (cooldownSec > 5) {
|
|
310
|
+
// Even if we also need a ticket, cooldown takes priority — we can buy ticket later
|
|
311
|
+
if (needsTicket) {
|
|
312
|
+
LOG.info(`[adventure] Need ticket + on cooldown. Will buy ticket after cooldown.`);
|
|
313
|
+
}
|
|
314
|
+
return { result: 'cooldown', coins: 0, nextCooldownSec: cooldownSec + 3 };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── 4) If we need a ticket, try to buy one ─────────────────
|
|
318
|
+
if (needsTicket) {
|
|
319
|
+
LOG.warn(`[adventure] Need adventure ticket! Attempting to buy...`);
|
|
320
|
+
|
|
321
|
+
// Ticket costs 250,000 coins — check balance first to avoid wasting time in shop
|
|
322
|
+
const TICKET_COST = 250000;
|
|
323
|
+
let currentBalance = 0;
|
|
324
|
+
await channel.send('pls bal');
|
|
325
|
+
const balMsg = await waitForDankMemer(8000);
|
|
326
|
+
if (balMsg) {
|
|
327
|
+
currentBalance = parseBalance(balMsg);
|
|
328
|
+
LOG.info(`[adventure] Balance: ${c.yellow}⏣ ${currentBalance.toLocaleString()}${c.reset} (ticket costs ⏣ ${TICKET_COST.toLocaleString()})`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (currentBalance < TICKET_COST) {
|
|
332
|
+
LOG.warn(`[adventure] Not enough coins for ticket (⏣ ${currentBalance.toLocaleString()} < ⏣ ${TICKET_COST.toLocaleString()}). Grind more first.`);
|
|
333
|
+
return { result: `need ticket (⏣ ${currentBalance.toLocaleString()}/${TICKET_COST.toLocaleString()})`, coins: 0, nextCooldownSec: 120 };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const bought = await buyItem({
|
|
337
|
+
channel, waitForDankMemer, client,
|
|
338
|
+
itemName: 'Adventure Ticket', quantity: 1,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (!bought) {
|
|
342
|
+
LOG.error('[adventure] Could not buy adventure ticket from shop.');
|
|
343
|
+
return { result: 'need ticket (buy failed)', coins: 0, nextCooldownSec: 120 };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
LOG.success('[adventure] Tickets purchased! Re-running adventure...');
|
|
347
|
+
await sleep(1500);
|
|
348
|
+
|
|
349
|
+
await channel.send('pls adventure');
|
|
350
|
+
response = await waitForDankMemer(12000);
|
|
351
|
+
if (!response) return { result: 'no response after ticket buy', coins: 0, nextCooldownSec: null };
|
|
352
|
+
logMsg(response, 'adventure-after-buy');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ── Check if we're already mid-adventure (no select menu) ──
|
|
356
|
+
const menus = getAllSelectMenus(response);
|
|
357
|
+
const hasNextBtn = allButtons.some(b => (b.customId || '').includes('adventure-next'));
|
|
358
|
+
|
|
359
|
+
if (hasNextBtn && menus.length === 0) {
|
|
360
|
+
// Already mid-adventure from a previous run — jump straight to rounds
|
|
361
|
+
LOG.info('[adventure] Resuming mid-adventure...');
|
|
362
|
+
const { text: finalText, coins, interactions, rewards, finalMsg } = await playAdventureRounds(channel, response);
|
|
363
|
+
return buildResult(finalText, coins, interactions, rewards, finalMsg);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Select adventure type from dropdown ─────────────────────
|
|
367
|
+
if (menus.length > 0) {
|
|
368
|
+
// Re-fetch message to get hydrated components (minValues/maxValues)
|
|
369
|
+
const freshMsg = await channel.messages.fetch(response.id).catch(() => null);
|
|
370
|
+
if (freshMsg) response = freshMsg;
|
|
371
|
+
|
|
372
|
+
// Find the select menu row index
|
|
373
|
+
let menuRowIdx = -1;
|
|
374
|
+
for (let i = 0; i < (response.components || []).length; i++) {
|
|
375
|
+
const row = response.components[i];
|
|
376
|
+
for (const comp of row.components || []) {
|
|
377
|
+
if (comp.type === 'STRING_SELECT' || comp.type === 3) { menuRowIdx = i; break; }
|
|
378
|
+
}
|
|
379
|
+
if (menuRowIdx >= 0) break;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const menu = response.components[menuRowIdx]?.components[0];
|
|
383
|
+
const options = menu?.options || [];
|
|
384
|
+
if (options.length > 0 && menuRowIdx >= 0) {
|
|
385
|
+
lastAdventureIndex = (lastAdventureIndex + 1) % options.length;
|
|
386
|
+
const opt = options[lastAdventureIndex];
|
|
387
|
+
LOG.info(`[adventure] Selecting [${lastAdventureIndex + 1}/${options.length}]: "${opt.label}"`);
|
|
388
|
+
try {
|
|
389
|
+
const selectResult = await response.selectMenu(menuRowIdx, [opt.value]);
|
|
390
|
+
if (selectResult) {
|
|
391
|
+
response = selectResult;
|
|
392
|
+
logMsg(response, 'adventure-selected');
|
|
393
|
+
}
|
|
394
|
+
} catch (e) {
|
|
395
|
+
LOG.error(`[adventure] Select error: ${e.message}`);
|
|
396
|
+
}
|
|
397
|
+
await sleep(300);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── Click "Start" button ───────────────────────────────────
|
|
402
|
+
let updatedButtons = getAllButtons(response);
|
|
403
|
+
let startButton = updatedButtons.find(b =>
|
|
404
|
+
(b.label || '').toLowerCase() === 'start' ||
|
|
405
|
+
(b.customId || '').includes('adventure-backpack')
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
if (startButton && !startButton.disabled) {
|
|
409
|
+
LOG.info(`[adventure] Clicking "Start"...`);
|
|
410
|
+
const afterStart = await clickAndRefetch(channel, response, startButton);
|
|
411
|
+
if (afterStart) {
|
|
412
|
+
response = afterStart;
|
|
413
|
+
logMsg(response, 'adventure-after-start');
|
|
414
|
+
}
|
|
415
|
+
} else if (startButton && startButton.disabled) {
|
|
416
|
+
LOG.warn('[adventure] Start button disabled (no ticket?)');
|
|
417
|
+
return { result: 'start disabled (no ticket)', coins: 0, nextCooldownSec: 60 };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── Handle equip screen ────────────────────────────────────
|
|
421
|
+
updatedButtons = getAllButtons(response);
|
|
422
|
+
const equipAllBtn = updatedButtons.find(b => !b.disabled && (b.label || '').toLowerCase() === 'equip all');
|
|
423
|
+
const equipStartBtn = updatedButtons.find(b => !b.disabled && (b.label || '').toLowerCase() === 'start');
|
|
424
|
+
const hasCancelBtn = updatedButtons.some(b => !b.disabled && (b.label || '').toLowerCase() === 'cancel');
|
|
425
|
+
|
|
426
|
+
if (equipStartBtn && (equipAllBtn || hasCancelBtn)) {
|
|
427
|
+
LOG.info('[adventure] Equip screen detected');
|
|
428
|
+
|
|
429
|
+
if (equipAllBtn) {
|
|
430
|
+
LOG.info('[adventure] Equipping all items...');
|
|
431
|
+
const afterEquip = await clickAndRefetch(channel, response, equipAllBtn);
|
|
432
|
+
if (afterEquip) {
|
|
433
|
+
response = afterEquip;
|
|
434
|
+
logMsg(response, 'adventure-after-equip');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Click Start to begin the actual adventure
|
|
439
|
+
const newStart = getAllButtons(response).find(b =>
|
|
440
|
+
!b.disabled && (b.label || '').toLowerCase() === 'start'
|
|
441
|
+
);
|
|
442
|
+
if (newStart) {
|
|
443
|
+
LOG.info('[adventure] Starting adventure...');
|
|
444
|
+
const afterStart2 = await clickAndRefetch(channel, response, newStart);
|
|
445
|
+
if (afterStart2) {
|
|
446
|
+
response = afterStart2;
|
|
447
|
+
logMsg(response, 'adventure-started');
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ── Play through all adventure rounds ──────────────────────
|
|
453
|
+
const { text: finalText, coins, interactions, rewards, finalMsg } = await playAdventureRounds(channel, response);
|
|
454
|
+
return buildResult(finalText, coins, interactions, rewards, finalMsg);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ── Build result object with cooldown parsing ────────────────────
|
|
458
|
+
function buildResult(finalText, coins, interactions, rewards, msg) {
|
|
459
|
+
let nextCooldownSec = null;
|
|
460
|
+
|
|
461
|
+
// 1) Best: Unix timestamp <t:UNIX:t> in final text or embed
|
|
462
|
+
const allText = msg ? getFullText(msg) : finalText;
|
|
463
|
+
const tsMatch = allText.match(/<t:(\d+)(?::[tTdDfFR])?>/);
|
|
464
|
+
if (tsMatch) {
|
|
465
|
+
const unixTarget = parseInt(tsMatch[1]);
|
|
466
|
+
const nowUnix = Math.floor(Date.now() / 1000);
|
|
467
|
+
nextCooldownSec = Math.max(5, unixTarget - nowUnix);
|
|
468
|
+
LOG.info(`[adventure] Next at ${new Date(unixTarget * 1000).toLocaleTimeString()} (${c.yellow}${nextCooldownSec}s${c.reset})`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// 2) Fallback: "Adventure again in X minutes" button label
|
|
472
|
+
if (!nextCooldownSec && msg) {
|
|
473
|
+
for (const row of msg.components || []) {
|
|
474
|
+
for (const comp of row.components || []) {
|
|
475
|
+
const label = (comp.label || '').toLowerCase();
|
|
476
|
+
const btnMatch = label.match(/adventure again in (\d+)\s*(minute|min|hour|second)/);
|
|
477
|
+
if (btnMatch) {
|
|
478
|
+
nextCooldownSec = parseInt(btnMatch[1]);
|
|
479
|
+
const unit = btnMatch[2].toLowerCase();
|
|
480
|
+
if (unit.startsWith('min')) nextCooldownSec *= 60;
|
|
481
|
+
if (unit.startsWith('hour')) nextCooldownSec *= 3600;
|
|
482
|
+
LOG.info(`[adventure] Next in ${c.yellow}${btnMatch[1]} ${btnMatch[2]}s${c.reset} (from button)`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!nextCooldownSec) nextCooldownSec = 300;
|
|
489
|
+
|
|
490
|
+
let result;
|
|
491
|
+
if (coins > 0) {
|
|
492
|
+
result = `adventure (${interactions} interactions) → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset} | Rewards: ${rewards}`;
|
|
493
|
+
LOG.coin(`[adventure] Earned ${c.green}${c.bold}⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
494
|
+
} else {
|
|
495
|
+
result = `adventure done (${interactions} interactions) | Rewards: ${rewards}`;
|
|
496
|
+
LOG.info(`[adventure] Completed ${interactions} interactions`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return { result, coins, nextCooldownSec };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
module.exports = { runAdventure };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Beg command handler.
|
|
3
|
+
* Simple command: send "pls beg", parse coins from response.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { LOG, c, getFullText, parseCoins, logMsg, isHoldTight, getHoldTightReason, sleep } = require('./utils');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {object} opts
|
|
10
|
+
* @param {object} opts.channel
|
|
11
|
+
* @param {function} opts.waitForDankMemer
|
|
12
|
+
* @returns {Promise<{result: string, coins: number}>}
|
|
13
|
+
*/
|
|
14
|
+
async function runBeg({ channel, waitForDankMemer }) {
|
|
15
|
+
LOG.cmd(`${c.white}${c.bold}pls beg${c.reset}`);
|
|
16
|
+
|
|
17
|
+
await channel.send('pls beg');
|
|
18
|
+
const response = await waitForDankMemer(10000);
|
|
19
|
+
|
|
20
|
+
if (!response) {
|
|
21
|
+
LOG.warn('[beg] No response');
|
|
22
|
+
return { result: 'no response', coins: 0 };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (isHoldTight(response)) {
|
|
26
|
+
const reason = getHoldTightReason(response);
|
|
27
|
+
LOG.warn(`[beg] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
|
|
28
|
+
await sleep(30000);
|
|
29
|
+
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
logMsg(response, 'beg');
|
|
33
|
+
const text = getFullText(response);
|
|
34
|
+
const coins = parseCoins(text);
|
|
35
|
+
|
|
36
|
+
if (coins > 0) {
|
|
37
|
+
LOG.coin(`[beg] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
38
|
+
return { result: `beg → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`, coins };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
LOG.info(`[beg] ${text.substring(0, 80).replace(/\n/g, ' ')}`);
|
|
42
|
+
return { result: text.substring(0, 60) || 'done', coins: 0 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { runBeg };
|