dankgrinder 6.27.0 → 6.34.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/farm.js +127 -47
- package/package.json +1 -1
package/lib/commands/farm.js
CHANGED
|
@@ -84,22 +84,8 @@ function parseFarmGrowReadySec(text) {
|
|
|
84
84
|
const clean = String(stripAnsi(text || ''));
|
|
85
85
|
const lower = clean.toLowerCase();
|
|
86
86
|
|
|
87
|
-
// TEMP DEBUG: log what we're parsing
|
|
88
|
-
const hasHarvestReady = /ready to harvest|harvest ready|can be harvested/i.test(lower);
|
|
89
|
-
const hasWilt = /\bwilt/i.test(lower);
|
|
90
|
-
if (hasHarvestReady || hasWilt) {
|
|
91
|
-
// eslint-disable-next-line no-console
|
|
92
|
-
console.log('[parseFarmGrowReadySec DEBUG] hasHarvestReady=' + hasHarvestReady + ' hasWilt=' + hasWilt + ' clean=' + clean.slice(0, 300));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
87
|
// If crops are already harvestable, don't queue waiting.
|
|
96
|
-
if (/ready to harvest|harvest ready|can be harvested|
|
|
97
|
-
// eslint-disable-next-line no-console
|
|
98
|
-
console.log('[parseFarmGrowReadySec] MATCH — returning 0 (lower=' + lower.slice(0, 200) + ')');
|
|
99
|
-
return 0;
|
|
100
|
-
}
|
|
101
|
-
// eslint-disable-next-line no-console
|
|
102
|
-
console.log('[parseFarmGrowReadySec] NO MATCH — checking timestamp parse (lower=' + lower.slice(0, 200) + ')');
|
|
88
|
+
if (/ready to harvest|harvest ready|can be harvested|wilts?/i.test(lower)) return 0;
|
|
103
89
|
|
|
104
90
|
// If the farm is empty / shows plantable / needs tilling, no grow queue needed.
|
|
105
91
|
// This handles the post-harvest state where Dank Memer shows
|
|
@@ -164,6 +150,11 @@ const FARM_BUY_ALIASES = Object.freeze({
|
|
|
164
150
|
|
|
165
151
|
const FARM_TOTAL_SLOTS = 9;
|
|
166
152
|
const FARM_PHASE_ORDER = ['hoe', 'water', 'plant', 'harvest'];
|
|
153
|
+
// Seed preference: random among potato/beans group, else carrot, else highest-qty fallback
|
|
154
|
+
const SEED_PRIORITY_GROUPS = [
|
|
155
|
+
['potato', 'beans', 'bean'], // random pick when available
|
|
156
|
+
['carrot'], // fallback
|
|
157
|
+
];
|
|
167
158
|
|
|
168
159
|
function parseMissingFarmItem(text) {
|
|
169
160
|
const lower = String(stripAnsi(text || '')).toLowerCase();
|
|
@@ -556,12 +547,22 @@ function chooseSeedOption(menu) {
|
|
|
556
547
|
const withQty = options.map(o => {
|
|
557
548
|
const label = String(o.label || '');
|
|
558
549
|
const m = label.match(/\((\d{1,5})\)/);
|
|
559
|
-
return { option: o, qty: m ? parseInt(m[1], 10) : 0,
|
|
550
|
+
return { option: o, qty: m ? parseInt(m[1], 10) : 0, label };
|
|
560
551
|
});
|
|
561
552
|
|
|
562
|
-
|
|
563
|
-
|
|
553
|
+
// Try each priority group in order
|
|
554
|
+
for (const group of SEED_PRIORITY_GROUPS) {
|
|
555
|
+
const matches = withQty.filter(x =>
|
|
556
|
+
x.qty > 0 && group.some(s => x.label.toLowerCase().includes(s))
|
|
557
|
+
);
|
|
558
|
+
if (matches.length > 0) {
|
|
559
|
+
const pick = matches[Math.floor(Math.random() * matches.length)];
|
|
560
|
+
LOG.info(`[farm:plant] seed group=[${group.join('/')}] picked="${pick.label}" qty=${pick.qty}`);
|
|
561
|
+
return pick.option;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
564
|
|
|
565
|
+
// Fallback: highest qty
|
|
565
566
|
const best = withQty.sort((a, b) => b.qty - a.qty)[0];
|
|
566
567
|
return best?.option || options[0];
|
|
567
568
|
}
|
|
@@ -1058,7 +1059,7 @@ async function waitForEditedMessage(channel, messageId, baselineText, timeoutMs
|
|
|
1058
1059
|
// Determine the next action to take from the manage menu, given what the
|
|
1059
1060
|
// farm image vision says about the current state.
|
|
1060
1061
|
// Returns { action, button, reason } where action is null when the cycle is done.
|
|
1061
|
-
async function findNextFarmActionFromManage(msg, text, currentAction, imageAnalysis) {
|
|
1062
|
+
async function findNextFarmActionFromManage(msg, text, currentAction, imageAnalysis, forcedNextAction) {
|
|
1062
1063
|
const btns = getAllButtons(msg).filter(b => !b.disabled && !isNavOrUtilityButton(b));
|
|
1063
1064
|
const allBtns = getAllButtons(msg); // include disabled for state detection
|
|
1064
1065
|
if (btns.length === 0) return { action: null, button: null, reason: 'no-buttons' };
|
|
@@ -1066,6 +1067,20 @@ async function findNextFarmActionFromManage(msg, text, currentAction, imageAnaly
|
|
|
1066
1067
|
const managedActions = getManageActionButtons(msg);
|
|
1067
1068
|
const lower = String(stripAnsi(text || '')).toLowerCase();
|
|
1068
1069
|
|
|
1070
|
+
// If we were explicitly told to try a specific action (e.g., advancing from hoe→water),
|
|
1071
|
+
// skip the empty-farm default. If that action's button is also disabled,
|
|
1072
|
+
// cascade through the rest of the phase order.
|
|
1073
|
+
if (forcedNextAction) {
|
|
1074
|
+
const phaseOrder = FARM_PHASE_ORDER;
|
|
1075
|
+
const forcedIdx = phaseOrder.indexOf(forcedNextAction);
|
|
1076
|
+
// Try forced action first, then cascade through remaining phases
|
|
1077
|
+
for (let i = forcedIdx; i < phaseOrder.length; i++) {
|
|
1078
|
+
const phase = phaseOrder[i];
|
|
1079
|
+
const btn = managedActions[phase] || btns.find(b => hasAny(b, [phase]));
|
|
1080
|
+
if (btn) return { action: phase, button: btn, reason: `forced(${forcedNextAction}→${phase})` };
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1069
1084
|
// If farm is empty, always start with hoe.
|
|
1070
1085
|
if (/seems pretty empty|pretty empty|empty\.{0,3}/i.test(lower)) {
|
|
1071
1086
|
const btn = managedActions.hoe || btns.find(b => hasAny(b, ['hoe', 'till']));
|
|
@@ -1142,7 +1157,7 @@ async function findNextFarmActionFromManage(msg, text, currentAction, imageAnaly
|
|
|
1142
1157
|
async function advancePastConfirmation(response, waitForDankMemer) {
|
|
1143
1158
|
if (!response) return null;
|
|
1144
1159
|
|
|
1145
|
-
const CONFIRM_WORDS = ['continue', 'confirm', 'confirm all', 'done', 'back to farm', 'close'];
|
|
1160
|
+
const CONFIRM_WORDS = ['continue', 'confirm', 'confirm all', 'done', 'back to farm', 'back', 'close'];
|
|
1146
1161
|
const MAX_PAGES = 3;
|
|
1147
1162
|
|
|
1148
1163
|
for (let page = 0; page < MAX_PAGES; page++) {
|
|
@@ -1212,9 +1227,6 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1212
1227
|
await channel.send('pls farm view');
|
|
1213
1228
|
let response = await waitForDankMemer(12000);
|
|
1214
1229
|
|
|
1215
|
-
// eslint-disable-next-line no-console
|
|
1216
|
-
console.log('[farm] runFarm entered — response=' + (response ? 'got-msg' : 'null'));
|
|
1217
|
-
|
|
1218
1230
|
if (!response) {
|
|
1219
1231
|
LOG.warn('[farm] No response');
|
|
1220
1232
|
return { result: 'no response', coins: 0, nextCooldownSec: 90 };
|
|
@@ -1261,7 +1273,7 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1261
1273
|
const redisRecoveryMs = await getHarvestRecoveryMs(redis, accountId);
|
|
1262
1274
|
const redisRecovered = redisRecoveryMs === 0;
|
|
1263
1275
|
const growReadySec = parseFarmGrowReadySec(text);
|
|
1264
|
-
const textHasHarvestReady = /ready to harvest|harvest ready|can be harvested|
|
|
1276
|
+
const textHasHarvestReady = /ready to harvest|harvest ready|can be harvested|wilts?/i.test(lower);
|
|
1265
1277
|
if (textHasHarvestReady) LOG.info(`[farm] DETECTED harvest-ready in text (growReadySec=${growReadySec})`);
|
|
1266
1278
|
if (growReadySec > 0) LOG.info(`[farm] GROW-QUEUE DEBUG: lower=${lower.slice(0, 400)}`);
|
|
1267
1279
|
LOG.info(`[farm] grow-ready parse=${growReadySec == null ? 'none' : `${growReadySec}s`} redis_recovery=${redisRecoveryMs != null ? `${redisRecoveryMs}ms` : 'n/a'}`);
|
|
@@ -1275,8 +1287,6 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1275
1287
|
}
|
|
1276
1288
|
|
|
1277
1289
|
// Check for missing items on the initial farm view — buy and retry once.
|
|
1278
|
-
// eslint-disable-next-line no-console
|
|
1279
|
-
console.log('[farm] at vision check — text=' + brief(text, 200));
|
|
1280
1290
|
const farmVision0 = analyzeFarmState({ msg: response, text });
|
|
1281
1291
|
LOG.info(`[farm:vision] stage=${farmVision0.stage}${farmVision0.missing ? ` missing=${farmVision0.missing}` : ''}`);
|
|
1282
1292
|
if (farmVision0.missing) {
|
|
@@ -1296,8 +1306,6 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1296
1306
|
}
|
|
1297
1307
|
|
|
1298
1308
|
// ── Open the manage menu ───────────────────────────────────────────────────
|
|
1299
|
-
// eslint-disable-next-line no-console
|
|
1300
|
-
console.log('[farm] at manage-btn check — text length=' + text.length + ' hasManage=' + (findFarmButton(response, ['manage']) ? 'yes' : 'no'));
|
|
1301
1309
|
const manageBtn = findFarmButton(response, ['manage', 'farm-farm:manage']);
|
|
1302
1310
|
if (manageBtn) {
|
|
1303
1311
|
LOG.info('[farm] Opening Manage menu');
|
|
@@ -1330,21 +1338,19 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1330
1338
|
let lastAction = null;
|
|
1331
1339
|
let cycleResponse = response;
|
|
1332
1340
|
let actionsTaken = 0; // Track how many farm actions were actually executed
|
|
1341
|
+
let forcedNextAction = null; // When advancing a phase, force the next action check
|
|
1342
|
+
let lastApplyResp = null; // Track the last apply response for coin/cooldown parsing
|
|
1333
1343
|
|
|
1334
1344
|
while (cycleDepth < 5) {
|
|
1335
|
-
// eslint-disable-next-line no-console
|
|
1336
|
-
console.log('[farm] cycle iteration ' + cycleDepth + ' — cycleResponse type=' + (cycleResponse && cycleResponse.constructor ? cycleResponse.constructor.name : typeof cycleResponse));
|
|
1337
1345
|
let actionResult;
|
|
1338
1346
|
try {
|
|
1339
|
-
actionResult = await findNextFarmActionFromManage(cycleResponse, text, lastAction, initialAnalysis);
|
|
1347
|
+
actionResult = await findNextFarmActionFromManage(cycleResponse, text, lastAction, initialAnalysis, forcedNextAction);
|
|
1340
1348
|
} catch (e) {
|
|
1341
|
-
|
|
1342
|
-
console.log('[farm] findNextFarmActionFromManage ERROR:', e.message);
|
|
1349
|
+
LOG.warn(`[farm:cycle] findNextFarmActionFromManage error: ${e.message}`);
|
|
1343
1350
|
break;
|
|
1344
1351
|
}
|
|
1345
1352
|
const { action, button, reason } = actionResult;
|
|
1346
|
-
|
|
1347
|
-
console.log('[farm] findNextFarmActionFromManage result: action=' + action + ' reason=' + reason);
|
|
1353
|
+
forcedNextAction = null;
|
|
1348
1354
|
|
|
1349
1355
|
if (!action || !button) {
|
|
1350
1356
|
LOG.info(`[farm:cycle:${cycleDepth}] no action found (reason=${reason}) — breaking cycle`);
|
|
@@ -1397,37 +1403,93 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1397
1403
|
' all', 'all ', 'all)', 'all:', ':all',
|
|
1398
1404
|
'hoe all', 'water all', 'plant all', 'harvest all', 'confirm all',
|
|
1399
1405
|
]);
|
|
1406
|
+
|
|
1407
|
+
// If no enabled "All" button — check if it's disabled (farm is empty for this phase).
|
|
1408
|
+
// If disabled, treat as a no-op and set forcedNextAction for the next iteration.
|
|
1400
1409
|
if (!allBtn) {
|
|
1410
|
+
const allBtnsInclDisabled = getAllButtons(cycleResponse);
|
|
1411
|
+
const disabledAllBtn = allBtnsInclDisabled.find(b => {
|
|
1412
|
+
const label = String(b.label || '').toLowerCase();
|
|
1413
|
+
return (label.includes('all') || label.includes('hoe all') || label.includes('water all') ||
|
|
1414
|
+
label.includes('plant all') || label.includes('harvest all') || label.includes('confirm')) && b.disabled;
|
|
1415
|
+
});
|
|
1416
|
+
if (disabledAllBtn) {
|
|
1417
|
+
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1418
|
+
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1419
|
+
LOG.info(`[farm:cycle:${cycleDepth}] ${action} all-btn disabled (no-op) — next=${nextPhase || 'done'}`);
|
|
1420
|
+
actionsTaken++;
|
|
1421
|
+
lastAction = action;
|
|
1422
|
+
forcedNextAction = nextPhase;
|
|
1423
|
+
cycleDepth++;
|
|
1424
|
+
if (!nextPhase) break;
|
|
1425
|
+
initialAnalysis = null;
|
|
1426
|
+
await sleep(300);
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1401
1429
|
LOG.info(`[farm:cycle:${cycleDepth}] no All button for ${action} — breaking cycle`);
|
|
1402
1430
|
break;
|
|
1403
1431
|
}
|
|
1404
1432
|
|
|
1405
1433
|
LOG.info(`[farm:cycle:${cycleDepth}] applying ${action} with "${allBtn.label || '?'}"`);
|
|
1406
1434
|
await humanDelay(90, 260);
|
|
1407
|
-
|
|
1435
|
+
lastApplyResp = await clickAndCapture({
|
|
1408
1436
|
channel, waitForDankMemer, response: cycleResponse,
|
|
1409
1437
|
button: allBtn, tag: `farm-cycle-${action}-apply`,
|
|
1410
1438
|
});
|
|
1411
1439
|
if (!applyResp) { LOG.warn('[farm:cycle] All click returned null'); break; }
|
|
1412
1440
|
|
|
1413
1441
|
// Step 5: advance past any confirmation screens back to the manage menu
|
|
1414
|
-
// Capture the confirmation screen text so we can detect no-ops.
|
|
1415
|
-
const confirmationText = brief(getFullText(applyResp), 200);
|
|
1416
1442
|
cycleResponse = await advancePastConfirmation(applyResp, waitForDankMemer);
|
|
1417
1443
|
if (!cycleResponse) { LOG.warn('[farm:cycle] confirmation advance returned null'); break; }
|
|
1418
|
-
// Action was successfully applied — count it
|
|
1419
1444
|
actionsTaken++;
|
|
1420
1445
|
lastAction = action;
|
|
1446
|
+
text = getFullText(cycleResponse);
|
|
1447
|
+
clean = brief(text, 600);
|
|
1448
|
+
logFarmState('after-confirm', cycleResponse);
|
|
1421
1449
|
|
|
1422
|
-
// If
|
|
1423
|
-
//
|
|
1424
|
-
|
|
1425
|
-
|
|
1450
|
+
// If confirmation screen is identical to post-confirmation screen,
|
|
1451
|
+
// check whether we're on a planted confirmation (Dank Memer planted crops
|
|
1452
|
+
// and shows "ready at X"). In that case, click "Back" to return to the
|
|
1453
|
+
// farm view, then continue to harvest.
|
|
1454
|
+
const confirmText = brief(getFullText(lastApplyResp), 200);
|
|
1455
|
+
const afterText = brief(getFullText(cycleResponse), 200);
|
|
1456
|
+
const afterLower = afterText.toLowerCase();
|
|
1457
|
+
const isPlantedConfirmation = /potato|seeds ready|planted|crop.*ready|ready.*\d/i.test(afterLower);
|
|
1458
|
+
const afterBtns = getAllButtons(cycleResponse);
|
|
1459
|
+
const backBtn = afterBtns.find(b => !b.disabled && /back|back to farm/i.test(String(b.label || '').toLowerCase()));
|
|
1460
|
+
|
|
1461
|
+
if (afterText === confirmText) {
|
|
1462
|
+
if (isPlantedConfirmation && backBtn) {
|
|
1463
|
+
// Plant was successful — click "Back" to return to farm view, then proceed.
|
|
1464
|
+
LOG.info(`[farm:cycle:${cycleDepth}] plant confirmed (crops planted) — clicking Back`);
|
|
1465
|
+
await humanDelay(80, 200);
|
|
1466
|
+
const backResp = await clickAndCapture({
|
|
1467
|
+
channel, waitForDankMemer, response: cycleResponse,
|
|
1468
|
+
button: backBtn, tag: `farm-cycle-${action}-back`,
|
|
1469
|
+
});
|
|
1470
|
+
if (backResp) {
|
|
1471
|
+
cycleResponse = backResp;
|
|
1472
|
+
text = getFullText(cycleResponse);
|
|
1473
|
+
clean = brief(text, 600);
|
|
1474
|
+
logFarmState('after-plant-back', cycleResponse);
|
|
1475
|
+
}
|
|
1476
|
+
// Plant succeeded — advance to harvest.
|
|
1477
|
+
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1478
|
+
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1479
|
+
LOG.info(`[farm:cycle:${cycleDepth}] plant done, advancing to ${nextPhase || 'done'}`);
|
|
1480
|
+
forcedNextAction = nextPhase;
|
|
1481
|
+
if (!nextPhase) break;
|
|
1482
|
+
cycleDepth++;
|
|
1483
|
+
lastAction = nextPhase;
|
|
1484
|
+
initialAnalysis = null;
|
|
1485
|
+
await sleep(400);
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
// Truly a no-op (e.g., hoe-ing already-tilled soil).
|
|
1426
1489
|
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1427
1490
|
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1428
|
-
LOG.info(`[farm:cycle:${cycleDepth}] ${action} was no-op —
|
|
1429
|
-
|
|
1430
|
-
clean = brief(text, 600);
|
|
1491
|
+
LOG.info(`[farm:cycle:${cycleDepth}] ${action} was no-op — next=${nextPhase || 'done'}`);
|
|
1492
|
+
forcedNextAction = nextPhase;
|
|
1431
1493
|
if (!nextPhase) break;
|
|
1432
1494
|
cycleDepth++;
|
|
1433
1495
|
lastAction = nextPhase;
|
|
@@ -1486,9 +1548,27 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1486
1548
|
}
|
|
1487
1549
|
|
|
1488
1550
|
// ── Parse final result ─────────────────────────────────────────────────────
|
|
1489
|
-
|
|
1551
|
+
// If the cycle ended with a planted confirmation (harvest→plant→confirmed),
|
|
1552
|
+
// the coins were shown on lastApplyResp (harvest confirmation). Parse from there too.
|
|
1553
|
+
let coins = parseCoins(text);
|
|
1554
|
+
if (coins <= 0 && lastApplyResp) {
|
|
1555
|
+
const applyCoins = parseCoins(getFullText(lastApplyResp));
|
|
1556
|
+
if (applyCoins > 0) coins = applyCoins;
|
|
1557
|
+
}
|
|
1490
1558
|
let nextCd = parseFarmCooldownSec(text) || 30;
|
|
1491
1559
|
const growReadyEnd = parseFarmGrowReadySec(text);
|
|
1560
|
+
// If the final screen is a planted confirmation, use its wilt timestamp as the
|
|
1561
|
+
// grow queue timer — the crops are planted and will be ready at that time.
|
|
1562
|
+
const finalLower = brief(text, 300).toLowerCase();
|
|
1563
|
+
const isFinalPlanted = /potato|seeds ready|planted|crop.*ready|ready.*\d/i.test(finalLower);
|
|
1564
|
+
if (isFinalPlanted && growReadyEnd === null) {
|
|
1565
|
+
// Crops were planted. The wilt timestamp in the planted confirmation tells us
|
|
1566
|
+
// when to harvest next. Set cooldown to that time, capped at a reasonable max.
|
|
1567
|
+
const plantedCd = parseFarmCooldownSec(text);
|
|
1568
|
+
if (plantedCd && plantedCd > 0) {
|
|
1569
|
+
nextCd = Math.max(nextCd, Math.min(6 * 3600, plantedCd + 2));
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1492
1572
|
if (Number.isFinite(growReadyEnd) && growReadyEnd > 0) {
|
|
1493
1573
|
nextCd = Math.max(nextCd, Math.min(6 * 3600, growReadyEnd + 2));
|
|
1494
1574
|
}
|