dankgrinder 6.46.0 → 7.6.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/beg.js +4 -26
- package/lib/commands/farm.js +17 -22
- package/lib/commands/farmVision.js +71 -107
- package/lib/commands/fishVision.js +1 -27
- package/lib/commands/index.js +2 -0
- package/lib/commands/inventory.js +1 -1
- package/lib/commands/scratch.js +83 -0
- package/lib/commands/utils.js +2 -4
- package/lib/grinder.js +303 -282
- package/lib/rawLogger.js +206 -102
- package/lib/structures.js +26 -19
- package/package.json +1 -1
package/lib/commands/beg.js
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Beg command handler.
|
|
3
3
|
* Simple command: send "pls beg", parse coins from response.
|
|
4
|
-
* Detects: positive coins, Life Saver drops, negative (no coins).
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
const { LOG, c, getFullText, parseCoins, logMsg, isHoldTight, getHoldTightReason, sleep } = require('./utils');
|
|
8
7
|
|
|
9
8
|
const RE_NEWLINE = /\n/g;
|
|
10
|
-
const RE_LIFE_SAVER = /life\s*saver/i;
|
|
11
|
-
const RE_BEG_COINS = /you\s+received\s*:\s*[\s\S]{0,200}?([\d,]+)/i;
|
|
12
9
|
|
|
13
10
|
/**
|
|
14
11
|
* @param {object} opts
|
|
15
12
|
* @param {object} opts.channel
|
|
16
13
|
* @param {function} opts.waitForDankMemer
|
|
17
|
-
* @returns {Promise<{result: string, coins: number
|
|
14
|
+
* @returns {Promise<{result: string, coins: number}>}
|
|
18
15
|
*/
|
|
19
16
|
async function runBeg({ channel, waitForDankMemer }) {
|
|
20
17
|
LOG.cmd(`${c.white}${c.bold}pls beg${c.reset}`);
|
|
@@ -36,30 +33,11 @@ async function runBeg({ channel, waitForDankMemer }) {
|
|
|
36
33
|
|
|
37
34
|
logMsg(response, 'beg');
|
|
38
35
|
const text = getFullText(response);
|
|
39
|
-
|
|
40
|
-
// Extract coins: prefer "You received" pattern for beg responses
|
|
41
|
-
let coins = 0;
|
|
42
|
-
const begMatch = text.match(RE_BEG_COINS);
|
|
43
|
-
if (begMatch) {
|
|
44
|
-
coins = parseInt(begMatch[1].replace(/,/g, ''), 10) || 0;
|
|
45
|
-
}
|
|
46
|
-
// Fallback to general parseCoins
|
|
47
|
-
if (coins === 0) {
|
|
48
|
-
coins = parseCoins(text);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Detect Life Saver drops
|
|
52
|
-
const lifeSaver = RE_LIFE_SAVER.test(text);
|
|
36
|
+
const coins = parseCoins(text);
|
|
53
37
|
|
|
54
38
|
if (coins > 0) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return { result: `beg → ${c.green}+⏣ ${coins.toLocaleString()}${lifeSaver ? ' + Life Saver' : ''}${c.reset}`, coins, lifeSaver };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (lifeSaver) {
|
|
61
|
-
LOG.coin(`[beg] ${c.cyan}Life Saver${c.reset} (no coins)`);
|
|
62
|
-
return { result: 'beg → Life Saver', coins: 0, lifeSaver };
|
|
39
|
+
LOG.coin(`[beg] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
40
|
+
return { result: `beg → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`, coins };
|
|
63
41
|
}
|
|
64
42
|
|
|
65
43
|
LOG.info(`[beg] ${text.substring(0, 80).replace(RE_NEWLINE, ' ')}`);
|
package/lib/commands/farm.js
CHANGED
|
@@ -474,8 +474,8 @@ function getVisionRepairPlan(actionName, score) {
|
|
|
474
474
|
}
|
|
475
475
|
if (actionName === 'hoe') {
|
|
476
476
|
const planted = score.ratios?.planted || 0;
|
|
477
|
-
const
|
|
478
|
-
if (
|
|
477
|
+
const tilled = score.ratios?.tilled || 0;
|
|
478
|
+
if (planted >= 0.30 || tilled >= 0.30) {
|
|
479
479
|
// Farm likely already progressed beyond hoe stage.
|
|
480
480
|
return null;
|
|
481
481
|
}
|
|
@@ -488,10 +488,9 @@ function getVisionRepairPlan(actionName, score) {
|
|
|
488
488
|
function inferNextActionFromScore(actionName, score) {
|
|
489
489
|
if (!score) return null;
|
|
490
490
|
const planted = score.ratios?.planted || 0;
|
|
491
|
-
const wet = score.ratios?.wet || 0;
|
|
492
491
|
const tilled = score.ratios?.tilled || 0;
|
|
493
492
|
|
|
494
|
-
if (actionName === 'hoe' && (planted +
|
|
493
|
+
if (actionName === 'hoe' && (planted + tilled) >= 0.45) {
|
|
495
494
|
return 'water';
|
|
496
495
|
}
|
|
497
496
|
if (actionName === 'water' && planted >= 0.45) {
|
|
@@ -515,16 +514,14 @@ async function inferPreferredActionFromImage(msg) {
|
|
|
515
514
|
const slots = Math.max(1, (analysis?.rows || 3) * (analysis?.cols || 3));
|
|
516
515
|
const ratios = {
|
|
517
516
|
tilled: (analysis?.counts?.tilled || 0) / slots,
|
|
518
|
-
wet: (analysis?.counts?.wet || 0) / slots,
|
|
519
517
|
planted: (analysis?.counts?.planted || 0) / slots,
|
|
520
518
|
unknown: (analysis?.counts?.unknown || 0) / slots,
|
|
521
519
|
};
|
|
522
520
|
|
|
523
521
|
let suggested = null;
|
|
524
|
-
// Image-first phase routing.
|
|
525
|
-
if (ratios.planted >= 0.
|
|
526
|
-
else if (ratios.
|
|
527
|
-
else if (ratios.tilled >= 0.35) suggested = 'water';
|
|
522
|
+
// Image-first phase routing (3-state: tilled/planted/unknown, no separate wet state).
|
|
523
|
+
if (ratios.planted >= 0.30) suggested = 'plant';
|
|
524
|
+
else if (ratios.tilled >= 0.30) suggested = 'water';
|
|
528
525
|
else suggested = 'hoe';
|
|
529
526
|
|
|
530
527
|
let dbg = null;
|
|
@@ -541,7 +538,6 @@ async function inferPreferredActionFromImage(msg) {
|
|
|
541
538
|
|
|
542
539
|
LOG.info(`[farm:phase-image] url=${url} grid=${gridToString(analysis)} counts=${JSON.stringify(analysis.counts)} conf=${analysis.avgConfidence} suggested=${suggested} ratios=${JSON.stringify({
|
|
543
540
|
tilled: +ratios.tilled.toFixed(3),
|
|
544
|
-
wet: +ratios.wet.toFixed(3),
|
|
545
541
|
planted: +ratios.planted.toFixed(3),
|
|
546
542
|
unknown: +ratios.unknown.toFixed(3),
|
|
547
543
|
})}`);
|
|
@@ -1117,31 +1113,30 @@ async function findNextFarmActionFromManage(msg, text, currentAction, imageAnaly
|
|
|
1117
1113
|
const slots = Math.max(1, (imageAnalysis.rows || 3) * (imageAnalysis.cols || 3));
|
|
1118
1114
|
const ratios = {
|
|
1119
1115
|
tilled: (imageAnalysis.counts?.tilled || 0) / slots,
|
|
1120
|
-
wet: (imageAnalysis.counts?.wet || 0) / slots,
|
|
1121
1116
|
planted: (imageAnalysis.counts?.planted || 0) / slots,
|
|
1122
1117
|
unknown: (imageAnalysis.counts?.unknown || 0) / slots,
|
|
1123
1118
|
};
|
|
1124
1119
|
|
|
1125
|
-
// Phase-aware selection: pick the earliest incomplete phase.
|
|
1126
|
-
// Harvest when planted >=
|
|
1127
|
-
if (ratios.planted >= 0.
|
|
1120
|
+
// Phase-aware selection: pick the earliest incomplete phase (3-state model).
|
|
1121
|
+
// Harvest when planted >= 30%.
|
|
1122
|
+
if (ratios.planted >= 0.30) {
|
|
1128
1123
|
const btn = managedActions.harvest || btns.find(b => hasAny(b, ['harvest', 'reap', 'collect']));
|
|
1129
1124
|
if (btn) return { action: 'harvest', button: btn, reason: `harvest-ready-vision(planted=${ratios.planted.toFixed(2)})` };
|
|
1130
1125
|
}
|
|
1131
|
-
// Plant when
|
|
1132
|
-
if (ratios.
|
|
1126
|
+
// Plant when tilled >= 30%.
|
|
1127
|
+
if (ratios.tilled >= 0.30) {
|
|
1133
1128
|
const btn = managedActions.plant || btns.find(b => hasAny(b, ['plant', 'seed', 'sow']));
|
|
1134
|
-
if (btn) return { action: 'plant', button: btn, reason: `plant-vision(
|
|
1129
|
+
if (btn) return { action: 'plant', button: btn, reason: `plant-vision(tilled=${ratios.tilled.toFixed(2)})` };
|
|
1135
1130
|
}
|
|
1136
|
-
// Water when we have tilled tiles
|
|
1137
|
-
if (ratios.tilled >= 0.
|
|
1131
|
+
// Water when we have tilled tiles.
|
|
1132
|
+
if (ratios.tilled >= 0.15) {
|
|
1138
1133
|
const btn = managedActions.water || btns.find(b => hasAny(b, ['water', 'watering']));
|
|
1139
|
-
if (btn) return { action: 'water', button: btn, reason: `water-vision(tilled=${ratios.tilled.toFixed(2)}
|
|
1134
|
+
if (btn) return { action: 'water', button: btn, reason: `water-vision(tilled=${ratios.tilled.toFixed(2)})` };
|
|
1140
1135
|
}
|
|
1141
1136
|
// Hoe when farm is mostly unknown/empty.
|
|
1142
|
-
if (ratios.tilled < 0.
|
|
1137
|
+
if (ratios.tilled < 0.15 && ratios.planted < 0.30) {
|
|
1143
1138
|
const btn = managedActions.hoe || btns.find(b => hasAny(b, ['hoe', 'till']));
|
|
1144
|
-
if (btn) return { action: 'hoe', button: btn, reason: `hoe-vision(t=${ratios.tilled.toFixed(2)},
|
|
1139
|
+
if (btn) return { action: 'hoe', button: btn, reason: `hoe-vision(t=${ratios.tilled.toFixed(2)},p=${ratios.planted.toFixed(2)})` };
|
|
1145
1140
|
}
|
|
1146
1141
|
}
|
|
1147
1142
|
|
|
@@ -82,12 +82,16 @@ function extractFarmImageUrl(msg) {
|
|
|
82
82
|
function colorStatsForPixel(r, g, b) {
|
|
83
83
|
const sum = r + g + b;
|
|
84
84
|
const brightness = sum / 3;
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
const
|
|
85
|
+
// Crop green — harvest phase: large bright leaves, G significantly > R and B
|
|
86
|
+
const greenStrong = g > r * 1.25 && g > b * 1.25 && g > 38;
|
|
87
|
+
// Weeds/grass — small scattered green, less intense than crop green
|
|
88
|
+
const weedGreen = g > r * 1.12 && g > b * 1.1 && g <= 38;
|
|
89
|
+
// Wet soil — rich dark brown, no blue, higher R than typical dry soil
|
|
90
|
+
const wetSoil = r > 50 && r > g * 1.5 && b < 20 && brightness < 45;
|
|
91
|
+
// Dry tilled soil — medium brown, debris textures
|
|
92
|
+
const drySoil = r > 35 && r > g * 1.2 && b < 30;
|
|
89
93
|
const whiteish = brightness > 210 && Math.abs(r - g) < 20 && Math.abs(g - b) < 20;
|
|
90
|
-
return { brightness, greenStrong,
|
|
94
|
+
return { brightness, greenStrong, weedGreen, wetSoil, drySoil, whiteish };
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
function clamp01(n) {
|
|
@@ -97,52 +101,34 @@ function clamp01(n) {
|
|
|
97
101
|
function classifyCell(features) {
|
|
98
102
|
const { greenPct, bluePct, brownPct, darkPct, avgBrightness } = features;
|
|
99
103
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
(
|
|
105
|
-
|
|
106
|
-
const scoreWet =
|
|
107
|
-
(bluePct * 1.7) +
|
|
108
|
-
(darkPct * 0.7) +
|
|
109
|
-
(clamp01((105 - avgBrightness) / 105) * 0.15) -
|
|
110
|
-
(greenPct * 0.35);
|
|
111
|
-
|
|
112
|
-
const scoreTilled =
|
|
113
|
-
(brownPct * 1.9) +
|
|
114
|
-
(clamp01((125 - avgBrightness) / 125) * 0.1) -
|
|
115
|
-
(greenPct * 0.25) -
|
|
116
|
-
(bluePct * 0.2);
|
|
117
|
-
|
|
118
|
-
const scored = [
|
|
119
|
-
['planted', scorePlanted],
|
|
120
|
-
['wet', scoreWet],
|
|
121
|
-
['tilled', scoreTilled],
|
|
122
|
-
].sort((a, b) => b[1] - a[1]);
|
|
123
|
-
|
|
124
|
-
const [bestState, bestScore] = scored[0];
|
|
125
|
-
const secondScore = scored[1][1];
|
|
126
|
-
const margin = bestScore - secondScore;
|
|
127
|
-
|
|
128
|
-
// Confidence from score margin + minimum signal level.
|
|
129
|
-
const signal = Math.max(greenPct, bluePct, brownPct);
|
|
130
|
-
const confidence = clamp01((margin * 2.1) + (signal * 0.6));
|
|
104
|
+
// ── HARVEST: Bright green mature crops (G >> R,B, G>38) ──
|
|
105
|
+
// Detected by greenStrong pixel percentage — crop leaves are vivid green
|
|
106
|
+
if (greenPct >= 0.30) {
|
|
107
|
+
const confidence = clamp01(0.5 + (greenPct - 0.30) * 2.0);
|
|
108
|
+
return { state: 'planted', confidence: +confidence.toFixed(3), scores: { planted: +greenPct.toFixed(3), wet: +darkPct.toFixed(3), tilled: +brownPct.toFixed(3) } };
|
|
109
|
+
}
|
|
131
110
|
|
|
132
|
-
//
|
|
133
|
-
if (
|
|
134
|
-
|
|
111
|
+
// ── TILLED / WET: Brown soil — no meaningful green or blue ──
|
|
112
|
+
if (brownPct >= 0.15) {
|
|
113
|
+
// High brown percentage = definitely soil. Both hoe (dry) and water (wet)
|
|
114
|
+
// phases look like brown soil. We classify as 'tilled' — the farming
|
|
115
|
+
// logic handles water vs hoe state transition by phase context.
|
|
116
|
+
const confidence = clamp01((brownPct - 0.15) * 2.5 + (1 - darkPct) * 0.3);
|
|
117
|
+
return { state: 'tilled', confidence: +confidence.toFixed(3), scores: { planted: +greenPct.toFixed(3), wet: +darkPct.toFixed(3), tilled: +brownPct.toFixed(3) } };
|
|
135
118
|
}
|
|
136
119
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
120
|
+
// ── PLANTED (early stage): Soil with green weeds ──
|
|
121
|
+
// Cells with 5-30% green are likely soil with weeds/crops starting.
|
|
122
|
+
// This is distinct from harvest (30%+) which is handled above.
|
|
123
|
+
if (greenPct >= 0.05 && greenPct < 0.30) {
|
|
124
|
+
const confidence = clamp01((greenPct - 0.05) * 3.0);
|
|
125
|
+
return { state: 'planted', confidence: +confidence.toFixed(3), scores: { planted: +greenPct.toFixed(3), wet: +darkPct.toFixed(3), tilled: +brownPct.toFixed(3) } };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── UNKNOWN: Low signal — grass borders, artifacts, etc. ──
|
|
129
|
+
const signal = Math.max(greenPct, bluePct, brownPct);
|
|
130
|
+
const confidence = signal * 1.5;
|
|
131
|
+
return { state: 'unknown', confidence: +confidence.toFixed(3), scores: { planted: +greenPct.toFixed(3), wet: +darkPct.toFixed(3), tilled: +brownPct.toFixed(3) } };
|
|
146
132
|
}
|
|
147
133
|
|
|
148
134
|
async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
|
|
@@ -160,18 +146,17 @@ async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
|
|
|
160
146
|
const ey = Math.min(sy + cellH, height);
|
|
161
147
|
|
|
162
148
|
// Sample center region only (ignore grass borders around each tile).
|
|
163
|
-
const padX = Math.floor((ex - sx) * 0.
|
|
164
|
-
const padY = Math.floor((ey - sy) * 0.
|
|
149
|
+
const padX = Math.floor((ex - sx) * 0.20); // 20% padding to cut more grass
|
|
150
|
+
const padY = Math.floor((ey - sy) * 0.20);
|
|
165
151
|
const csx = sx + padX;
|
|
166
152
|
const csy = sy + padY;
|
|
167
153
|
const cex = ex - padX;
|
|
168
154
|
const cey = ey - padY;
|
|
169
155
|
|
|
170
156
|
let total = 0;
|
|
171
|
-
let greenPx = 0;
|
|
172
|
-
let
|
|
173
|
-
let
|
|
174
|
-
let darkPx = 0;
|
|
157
|
+
let greenPx = 0; // crop green: G > R*1.25 && G > B*1.25 && G > 38
|
|
158
|
+
let brownPx = 0; // soil brown: R > G*1.2 && B < 30
|
|
159
|
+
let darkPx = 0; // very dark: brightness < 56
|
|
175
160
|
let whitePx = 0;
|
|
176
161
|
let brightSum = 0;
|
|
177
162
|
|
|
@@ -184,22 +169,20 @@ async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
|
|
|
184
169
|
const s = colorStatsForPixel(r, g, b);
|
|
185
170
|
total++;
|
|
186
171
|
brightSum += s.brightness;
|
|
187
|
-
if (s.greenStrong) greenPx++;
|
|
188
|
-
if (s.
|
|
189
|
-
if (s.brownish) brownPx++;
|
|
172
|
+
if (s.greenStrong) greenPx++; // harvest green (vivid crop leaves)
|
|
173
|
+
if (s.drySoil || s.wetSoil) brownPx++; // soil — dry or wet
|
|
190
174
|
if (s.dark) darkPx++;
|
|
191
175
|
if (s.whiteish) whitePx++;
|
|
192
176
|
}
|
|
193
177
|
}
|
|
194
178
|
|
|
195
179
|
const greenPct = greenPx / total;
|
|
196
|
-
const bluePct = bluePx / total;
|
|
197
180
|
const brownPct = brownPx / total;
|
|
198
181
|
const darkPct = darkPx / total;
|
|
199
182
|
const whitePct = whitePx / total;
|
|
200
183
|
const avgBrightness = brightSum / total;
|
|
201
184
|
|
|
202
|
-
const classified = classifyCell({ greenPct,
|
|
185
|
+
const classified = classifyCell({ greenPct, brownPct, darkPct, avgBrightness });
|
|
203
186
|
|
|
204
187
|
cells.push({
|
|
205
188
|
row,
|
|
@@ -208,16 +191,14 @@ async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
|
|
|
208
191
|
confidence: classified.confidence,
|
|
209
192
|
scores: classified.scores,
|
|
210
193
|
greenPct: +greenPct.toFixed(3),
|
|
211
|
-
bluePct: +bluePct.toFixed(3),
|
|
212
194
|
brownPct: +brownPct.toFixed(3),
|
|
213
195
|
darkPct: +darkPct.toFixed(3),
|
|
214
|
-
whitePct: +whitePct.toFixed(3),
|
|
215
196
|
avgBrightness: +avgBrightness.toFixed(1),
|
|
216
197
|
});
|
|
217
198
|
}
|
|
218
199
|
}
|
|
219
200
|
|
|
220
|
-
const counts = { tilled: 0,
|
|
201
|
+
const counts = { tilled: 0, planted: 0, unknown: 0 };
|
|
221
202
|
for (const c of cells) counts[c.state] = (counts[c.state] || 0) + 1;
|
|
222
203
|
|
|
223
204
|
const avgConfidence = cells.length > 0
|
|
@@ -228,7 +209,7 @@ async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
|
|
|
228
209
|
}
|
|
229
210
|
|
|
230
211
|
function gridToString(analysis) {
|
|
231
|
-
const icon = { tilled: 'T',
|
|
212
|
+
const icon = { tilled: 'T', planted: 'P', unknown: '?' };
|
|
232
213
|
const rows = [];
|
|
233
214
|
for (let r = 0; r < (analysis?.rows || 0); r++) {
|
|
234
215
|
const line = [];
|
|
@@ -251,25 +232,23 @@ function evaluateActionNeed(actionName, analysis) {
|
|
|
251
232
|
const slots = 9;
|
|
252
233
|
|
|
253
234
|
// Avoid aggressive repeats when confidence is too low.
|
|
254
|
-
if (conf < 0.
|
|
235
|
+
if (conf < 0.20) return false;
|
|
255
236
|
|
|
256
|
-
//
|
|
237
|
+
// Simple rules:
|
|
238
|
+
// - hoe: repeat until farm has enough tilled/planted soil
|
|
239
|
+
// - water: skip (we don't distinguish wet vs tilled visually — use phase context instead)
|
|
240
|
+
// - plant: repeat until most cells show green crops
|
|
241
|
+
// - harvest: repeat if any planted crops visible
|
|
257
242
|
if (actionName === 'hoe') {
|
|
258
|
-
return (tilled +
|
|
259
|
-
}
|
|
260
|
-
if (actionName === 'water') {
|
|
261
|
-
// If still many plain-tilled (not watered/planted), water likely incomplete.
|
|
262
|
-
return tilled > 0 && (wet + planted) < slots;
|
|
243
|
+
return (tilled + planted) < slots && unknown < 6;
|
|
263
244
|
}
|
|
264
245
|
if (actionName === 'plant') {
|
|
265
|
-
return planted < slots && (tilled
|
|
246
|
+
return planted < slots && (tilled) > 0;
|
|
266
247
|
}
|
|
267
248
|
if (actionName === 'harvest') {
|
|
268
|
-
// If many planted remain, harvest may still be pending.
|
|
269
249
|
return planted > 0;
|
|
270
250
|
}
|
|
271
|
-
|
|
272
|
-
// Fertilizer optional; do not force repeats.
|
|
251
|
+
// water and others: don't force repeats
|
|
273
252
|
return false;
|
|
274
253
|
}
|
|
275
254
|
|
|
@@ -277,59 +256,45 @@ function evaluateActionScores(actionName, analysis) {
|
|
|
277
256
|
const c = analysis?.counts || {};
|
|
278
257
|
const slots = Math.max(1, (analysis?.rows || 3) * (analysis?.cols || 3));
|
|
279
258
|
const tilled = c.tilled || 0;
|
|
280
|
-
const wet = c.wet || 0;
|
|
281
259
|
const planted = c.planted || 0;
|
|
282
260
|
const unknown = c.unknown || 0;
|
|
283
261
|
const conf = analysis?.avgConfidence || 0;
|
|
284
262
|
|
|
285
263
|
const ratios = {
|
|
286
264
|
tilled: tilled / slots,
|
|
287
|
-
wet: wet / slots,
|
|
288
265
|
planted: planted / slots,
|
|
289
266
|
unknown: unknown / slots,
|
|
290
267
|
};
|
|
291
268
|
|
|
292
269
|
const clamp = (n) => Math.max(0, Math.min(1, n));
|
|
293
|
-
const withConf = (base
|
|
270
|
+
const withConf = (base) => clamp(base * (0.85 + conf * 0.3));
|
|
294
271
|
|
|
295
272
|
let score = 0;
|
|
296
|
-
let threshold = 0.
|
|
273
|
+
let threshold = 0.7;
|
|
297
274
|
let reason = 'ok';
|
|
298
275
|
|
|
299
276
|
if (actionName === 'hoe') {
|
|
300
|
-
// Hoe is
|
|
301
|
-
const base = (ratios.tilled * 0.9) + (ratios.
|
|
302
|
-
score = withConf(base);
|
|
303
|
-
threshold = 0.68;
|
|
304
|
-
if (ratios.unknown > 0.45) reason = 'too_many_unknown_cells';
|
|
305
|
-
else if (ratios.tilled < 0.45 && (ratios.wet + ratios.planted) < 0.35) reason = 'not_enough_tilled_or_progressed_tiles';
|
|
306
|
-
} else if (actionName === 'water') {
|
|
307
|
-
// After watering, we want wet/planted dominance and minimal plain-tilled.
|
|
308
|
-
const base = (ratios.wet * 0.92) + (ratios.planted * 0.34) - (ratios.tilled * 0.48) - (ratios.unknown * 0.22);
|
|
277
|
+
// Hoe is good if farm has mostly soil (tilled or planted)
|
|
278
|
+
const base = (ratios.tilled * 0.9) + (ratios.planted * 0.5) - (ratios.unknown * 0.4);
|
|
309
279
|
score = withConf(base);
|
|
310
|
-
threshold = 0.
|
|
311
|
-
if (ratios.
|
|
312
|
-
|
|
313
|
-
score = Math.max(score, withConf(0.86));
|
|
314
|
-
reason = 'progressed_to_plant_phase';
|
|
315
|
-
}
|
|
316
|
-
if (ratios.wet < 0.45 && ratios.planted < 0.2) reason = 'not_enough_wet_tiles';
|
|
317
|
-
else if (ratios.tilled > 0.4) reason = 'too_many_dry_tilled_tiles';
|
|
280
|
+
threshold = 0.55;
|
|
281
|
+
if (ratios.unknown > 0.5) reason = 'too_many_unknown_cells';
|
|
282
|
+
else if (ratios.tilled + ratios.planted < 0.4) reason = 'farm_not_ready';
|
|
318
283
|
} else if (actionName === 'plant') {
|
|
319
|
-
// Plant
|
|
320
|
-
const base = (ratios.planted * 0.
|
|
284
|
+
// Plant should end with mostly planted cells
|
|
285
|
+
const base = (ratios.planted * 0.98) - (ratios.tilled * 0.3) - (ratios.unknown * 0.2);
|
|
321
286
|
score = withConf(base);
|
|
322
|
-
threshold = 0.
|
|
323
|
-
if (ratios.planted < 0.
|
|
287
|
+
threshold = 0.65;
|
|
288
|
+
if (ratios.planted < 0.5) reason = 'not_enough_planted_tiles';
|
|
324
289
|
} else if (actionName === 'harvest') {
|
|
325
|
-
// Harvest
|
|
326
|
-
const base = (
|
|
290
|
+
// Harvest is good if planted tiles dominate
|
|
291
|
+
const base = (ratios.planted * 0.9) - (ratios.tilled * 0.1) - (ratios.unknown * 0.2);
|
|
327
292
|
score = withConf(base);
|
|
328
|
-
threshold = 0.
|
|
329
|
-
if (ratios.planted
|
|
293
|
+
threshold = 0.5;
|
|
294
|
+
if (ratios.planted < 0.3) reason = 'no_plants_to_harvest';
|
|
330
295
|
} else {
|
|
331
|
-
score = withConf(0.
|
|
332
|
-
threshold = 0.
|
|
296
|
+
score = withConf(0.6 - ratios.unknown * 0.3);
|
|
297
|
+
threshold = 0.6;
|
|
333
298
|
}
|
|
334
299
|
|
|
335
300
|
const matched = conf >= 0.2 && score >= threshold;
|
|
@@ -341,10 +306,9 @@ function evaluateActionScores(actionName, analysis) {
|
|
|
341
306
|
threshold: +threshold.toFixed(3),
|
|
342
307
|
confidence: +conf.toFixed(3),
|
|
343
308
|
reason,
|
|
344
|
-
counts: { tilled,
|
|
309
|
+
counts: { tilled, planted, unknown, slots },
|
|
345
310
|
ratios: {
|
|
346
311
|
tilled: +ratios.tilled.toFixed(3),
|
|
347
|
-
wet: +ratios.wet.toFixed(3),
|
|
348
312
|
planted: +ratios.planted.toFixed(3),
|
|
349
313
|
unknown: +ratios.unknown.toFixed(3),
|
|
350
314
|
},
|
|
@@ -11,27 +11,10 @@ const sharp = require('sharp');
|
|
|
11
11
|
const https = require('https');
|
|
12
12
|
const http = require('http');
|
|
13
13
|
|
|
14
|
-
// Allowed CDN hostnames for image downloads (prevents SSRF).
|
|
15
|
-
const ALLOWED_IMAGE_HOSTS = new Set([
|
|
16
|
-
'cdn.discordapp.com',
|
|
17
|
-
'media.discordapp.net',
|
|
18
|
-
'images.discordapp.net',
|
|
19
|
-
]);
|
|
20
|
-
|
|
21
|
-
const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB cap — prevents memory exhaustion
|
|
22
|
-
|
|
23
14
|
/**
|
|
24
15
|
* Download an image from a URL and return as Buffer.
|
|
25
|
-
* Only allows Discord CDN URLs and enforces a max size limit.
|
|
26
16
|
*/
|
|
27
17
|
function downloadImage(url) {
|
|
28
|
-
// ── SSRF check ──────────────────────────────────────────────
|
|
29
|
-
let hostname;
|
|
30
|
-
try { hostname = new URL(url).hostname; } catch { return Promise.reject(new Error('invalid URL')); }
|
|
31
|
-
if (!ALLOWED_IMAGE_HOSTS.has(hostname)) {
|
|
32
|
-
return Promise.reject(new Error(`disallowed host: ${hostname}`));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
18
|
return new Promise((resolve, reject) => {
|
|
36
19
|
const proto = url.startsWith('https') ? https : http;
|
|
37
20
|
const req = proto.get(url, res => {
|
|
@@ -39,16 +22,7 @@ function downloadImage(url) {
|
|
|
39
22
|
return downloadImage(res.headers.location).then(resolve, reject);
|
|
40
23
|
}
|
|
41
24
|
const chunks = [];
|
|
42
|
-
|
|
43
|
-
res.on('data', c => {
|
|
44
|
-
bytesReceived += c.length;
|
|
45
|
-
if (bytesReceived > MAX_IMAGE_BYTES) {
|
|
46
|
-
req.destroy();
|
|
47
|
-
reject(new Error(`image too large: ${bytesReceived} bytes (max ${MAX_IMAGE_BYTES})`));
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
chunks.push(c);
|
|
51
|
-
});
|
|
25
|
+
res.on('data', c => chunks.push(c));
|
|
52
26
|
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
53
27
|
res.on('error', reject);
|
|
54
28
|
});
|
package/lib/commands/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const { runHunt } = require('./hunt');
|
|
|
13
13
|
const { runDig } = require('./dig');
|
|
14
14
|
const { runFish, sellAllFish } = require('./fish');
|
|
15
15
|
const { runPostMemes } = require('./postmemes');
|
|
16
|
+
const { runScratch } = require('./scratch');
|
|
16
17
|
const { runBlackjack } = require('./blackjack');
|
|
17
18
|
const { runTrivia, triviaDB } = require('./trivia');
|
|
18
19
|
const { runWorkShift } = require('./work');
|
|
@@ -38,6 +39,7 @@ module.exports = {
|
|
|
38
39
|
runFish,
|
|
39
40
|
sellAllFish,
|
|
40
41
|
runPostMemes,
|
|
42
|
+
runScratch,
|
|
41
43
|
runBlackjack,
|
|
42
44
|
runTrivia,
|
|
43
45
|
runWorkShift,
|
|
@@ -198,7 +198,7 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
198
198
|
LOG.cmd(`${c.white}${c.bold}pls inv${c.reset}`);
|
|
199
199
|
|
|
200
200
|
await channel.send('pls inv');
|
|
201
|
-
let response = await waitForDankMemer(
|
|
201
|
+
let response = await waitForDankMemer(10000);
|
|
202
202
|
|
|
203
203
|
if (!response) {
|
|
204
204
|
LOG.warn('[inv] No response');
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scratch command handler.
|
|
3
|
+
* Send "pls scratch", click through scratch card buttons.
|
|
4
|
+
* Requires level 25 — checks profile level before running.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
|
|
9
|
+
logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
|
|
10
|
+
} = require('./utils');
|
|
11
|
+
const { meetsLevelRequirement } = require('./profile');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} opts
|
|
15
|
+
* @param {object} opts.channel
|
|
16
|
+
* @param {function} opts.waitForDankMemer
|
|
17
|
+
* @param {string} [opts.accountId]
|
|
18
|
+
* @param {object} [opts.redis]
|
|
19
|
+
* @returns {Promise<{result: string, coins: number}>}
|
|
20
|
+
*/
|
|
21
|
+
async function runScratch({ channel, waitForDankMemer, accountId, redis }) {
|
|
22
|
+
// Check level 25 requirement before wasting a command
|
|
23
|
+
const canRun = await meetsLevelRequirement({ channel, waitForDankMemer, accountId, redis }, 25);
|
|
24
|
+
if (!canRun) {
|
|
25
|
+
LOG.warn(`[scratch] Skipped — need level 25`);
|
|
26
|
+
return { result: 'skipped (need level 25)', coins: 0, skipReason: 'level' };
|
|
27
|
+
}
|
|
28
|
+
LOG.cmd(`${c.white}${c.bold}pls scratch${c.reset}`);
|
|
29
|
+
|
|
30
|
+
await channel.send('pls scratch');
|
|
31
|
+
const response = await waitForDankMemer(10000);
|
|
32
|
+
|
|
33
|
+
if (!response) {
|
|
34
|
+
LOG.warn('[scratch] No response');
|
|
35
|
+
return { result: 'no response', coins: 0 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isHoldTight(response)) {
|
|
39
|
+
const reason = getHoldTightReason(response);
|
|
40
|
+
LOG.warn(`[scratch] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
|
|
41
|
+
await sleep(30000);
|
|
42
|
+
return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
logMsg(response, 'scratch');
|
|
46
|
+
const buttons = getAllButtons(response);
|
|
47
|
+
|
|
48
|
+
if (buttons.length === 0) {
|
|
49
|
+
const text = getFullText(response);
|
|
50
|
+
const coins = parseCoins(text);
|
|
51
|
+
if (coins > 0) {
|
|
52
|
+
LOG.coin(`[scratch] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
53
|
+
return { result: `scratch → +⏣ ${coins.toLocaleString()}`, coins };
|
|
54
|
+
}
|
|
55
|
+
return { result: text.substring(0, 60) || 'done', coins: 0 };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Click through all scratch card cells
|
|
59
|
+
let lastResponse = response;
|
|
60
|
+
let clickCount = 0;
|
|
61
|
+
for (let i = 0; i < Math.min(buttons.length, 9); i++) {
|
|
62
|
+
const btn = buttons[i];
|
|
63
|
+
if (btn && !btn.disabled) {
|
|
64
|
+
await humanDelay(300, 700);
|
|
65
|
+
try {
|
|
66
|
+
const followUp = await safeClickButton(lastResponse, btn);
|
|
67
|
+
if (followUp) { lastResponse = followUp; clickCount++; }
|
|
68
|
+
} catch { break; }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const finalText = getFullText(lastResponse);
|
|
73
|
+
const coins = parseCoins(finalText);
|
|
74
|
+
|
|
75
|
+
if (coins > 0) {
|
|
76
|
+
LOG.coin(`[scratch] ${clickCount} clicks → ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|
|
77
|
+
return { result: `scratch (${clickCount} clicks) → +⏣ ${coins.toLocaleString()}`, coins };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { result: `scratch done (${clickCount} clicks)`, coins: 0 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { runScratch };
|
package/lib/commands/utils.js
CHANGED
|
@@ -558,8 +558,7 @@ async function clickCV2Button(msg, customId) {
|
|
|
558
558
|
const gId = msg.guildId || msg.guild?.id;
|
|
559
559
|
if (!token) throw new Error('No token for CV2 click');
|
|
560
560
|
const sessionId = msg.client?.ws?.shards?.first?.()?.sessionId;
|
|
561
|
-
const {
|
|
562
|
-
const nonce = randomUUID();
|
|
561
|
+
const nonce = `${(BigInt(Date.now() - 1420070400000) << 22n)}`;
|
|
563
562
|
const payloadObj = {
|
|
564
563
|
type: 3,
|
|
565
564
|
application_id: String(msg.applicationId || DANK_MEMER_ID),
|
|
@@ -604,8 +603,7 @@ async function clickCV2SelectMenu(msg, customId, values = []) {
|
|
|
604
603
|
const gId = msg.guildId || msg.guild?.id;
|
|
605
604
|
if (!token) throw new Error('No token for CV2 select');
|
|
606
605
|
const sessionId = msg.client?.ws?.shards?.first?.()?.sessionId;
|
|
607
|
-
const {
|
|
608
|
-
const nonce = randomUUID();
|
|
606
|
+
const nonce = `${BigInt(Date.now() - 1420070400000) << 22n}`;
|
|
609
607
|
const payloadObj = {
|
|
610
608
|
type: 3,
|
|
611
609
|
application_id: String(msg.applicationId || DANK_MEMER_ID),
|