dankgrinder 7.1.0 → 7.7.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 +235 -31
- package/lib/commands/farmVision.js +198 -140
- package/lib/commands/utils.js +18 -4
- package/lib/grinder.js +65 -16
- package/lib/rawLogger.js +229 -102
- package/package.json +1 -1
|
@@ -82,12 +82,11 @@ 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
|
-
const
|
|
88
|
-
const dark = brightness < 56;
|
|
85
|
+
// Grayscale: removes hue confusion between dusty-brown and green
|
|
86
|
+
const gray = Math.round(r * 0.299 + g * 0.587 + b * 0.114);
|
|
87
|
+
const soil = r >= g && b < 30;
|
|
89
88
|
const whiteish = brightness > 210 && Math.abs(r - g) < 20 && Math.abs(g - b) < 20;
|
|
90
|
-
return { brightness,
|
|
89
|
+
return { brightness, gray, soil, whiteish };
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
function clamp01(n) {
|
|
@@ -95,59 +94,79 @@ function clamp01(n) {
|
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
function classifyCell(features) {
|
|
98
|
-
const {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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));
|
|
131
|
-
|
|
132
|
-
// Low-signal cells become unknown.
|
|
133
|
-
if (signal < 0.085 || confidence < 0.20) {
|
|
134
|
-
return { state: 'unknown', confidence: +confidence.toFixed(3), scores: { planted: +scorePlanted.toFixed(3), wet: +scoreWet.toFixed(3), tilled: +scoreTilled.toFixed(3) } };
|
|
97
|
+
const {
|
|
98
|
+
harvestPct, plantPct, soilPct,
|
|
99
|
+
avgBrightness, grayStd,
|
|
100
|
+
brightGrayPct, darkGrayPct,
|
|
101
|
+
avgSat, avgR, avgG, avgB,
|
|
102
|
+
gMinusR, discordUiPct,
|
|
103
|
+
} = features;
|
|
104
|
+
|
|
105
|
+
// If Discord UI dominates this cell, the stats are meaningless
|
|
106
|
+
if (discordUiPct > 0.5) {
|
|
107
|
+
return { state: 'unknown', confidence: 0.1, scores: { harvest: 0, planted: 0, soil: 0 } };
|
|
135
108
|
}
|
|
136
109
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
110
|
+
// Actual farm pixel analysis from live screenshot:
|
|
111
|
+
// Water cells: sat=0.15-0.20, B=14-21, darkPct=64-91%, G-R=-20 to +4
|
|
112
|
+
// Tilled cells: sat=0.22-0.25, B=12-17, darkPct=64-81%, G-R=-8 to +11
|
|
113
|
+
// Harvest cells: sat=0.22-0.25, B=12-17, darkPct=64-73%, G-R=+4 to +11, harv=41-46%
|
|
114
|
+
//
|
|
115
|
+
// Key insight: sat < 0.20 + B > 14 = water cells
|
|
116
|
+
// Higher sat (0.22+) + harvestPixels > 40% = harvest crops
|
|
117
|
+
// Higher sat (0.22+) + harv < 40% = tilled soil
|
|
118
|
+
|
|
119
|
+
const isLowSatWater = avgSat < 0.20 && avgB > 14; // wet/dark soil
|
|
120
|
+
const isHighSatTilled = avgSat >= 0.20; // tilled/planted soil
|
|
121
|
+
const isBright = brightGrayPct > 0.15; // has bright crop pixels
|
|
122
|
+
const isDarkUniform = darkGrayPct > 0.55 && grayStd < 20; // very dark uniform (wet soil)
|
|
123
|
+
|
|
124
|
+
// ── HARVEST: Bright crops + high green + sat ≥ 0.20 ──
|
|
125
|
+
// Growing/ready crops: sat 0.22+, G-R > 5, harvestPixels > 40%
|
|
126
|
+
if (avgSat >= 0.20 && gMinusR > 5 && harvestPct > 0.40 && isBright) {
|
|
127
|
+
const excess = gMinusR - 5;
|
|
128
|
+
const confidence = clamp01(0.5 + excess * 0.06);
|
|
129
|
+
return { state: 'harvest', confidence: +confidence.toFixed(3), scores: { harvest: +harvestPct.toFixed(3), planted: +plantPct.toFixed(3), soil: +soilPct.toFixed(3) } };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── WATER: Low saturation + blue dominant (wet soil) ──
|
|
133
|
+
// sat < 0.20 AND B > 14 catches actual wet cells from live screenshot
|
|
134
|
+
if (isLowSatWater || isDarkUniform) {
|
|
135
|
+
return { state: 'water', confidence: +clamp01(0.5 + darkGrayPct * 0.3).toFixed(3), scores: { harvest: +harvestPct.toFixed(3), planted: +plantPct.toFixed(3), soil: +soilPct.toFixed(3) } };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── TILLED: Higher saturation (≥ 0.20) = soil/debris ──
|
|
139
|
+
// sat ≥ 0.20 covers tilled dirt, debris, and planted cells
|
|
140
|
+
if (isHighSatTilled) {
|
|
141
|
+
// Planted if some harvest pixels and some brightness
|
|
142
|
+
if (plantPct > 0.15 || gMinusR > 1) {
|
|
143
|
+
const confidence = clamp01(0.3 + avgSat * 0.8 + brightGrayPct * 0.5);
|
|
144
|
+
return { state: 'planted', confidence: +confidence.toFixed(3), scores: { harvest: +harvestPct.toFixed(3), planted: +plantPct.toFixed(3), soil: +soilPct.toFixed(3) } };
|
|
145
|
+
}
|
|
146
|
+
const confidence = clamp01((avgSat - 0.15) * 1.5);
|
|
147
|
+
return { state: 'tilled', confidence: +confidence.toFixed(3), scores: { harvest: +harvestPct.toFixed(3), planted: +plantPct.toFixed(3), soil: +soilPct.toFixed(3) } };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── UNKNOWN: Low signal / empty ──
|
|
151
|
+
const signal = Math.max(harvestPct, plantPct, soilPct, avgSat);
|
|
152
|
+
const confidence = signal * 1.5;
|
|
153
|
+
return { state: 'unknown', confidence: +confidence.toFixed(3), scores: { harvest: +harvestPct.toFixed(3), planted: +plantPct.toFixed(3), soil: +soilPct.toFixed(3) } };
|
|
146
154
|
}
|
|
147
155
|
|
|
148
156
|
async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
|
|
149
|
-
|
|
150
|
-
const
|
|
157
|
+
// Convert to grayscale to eliminate hue confusion between dusty-brown and green
|
|
158
|
+
const grayBuf = await sharp(imgBuffer).grayscale().raw().toBuffer({ resolveWithObject: true });
|
|
159
|
+
const { data: grayData, info: grayInfo } = grayBuf;
|
|
160
|
+
const { width, height, channels } = grayInfo;
|
|
161
|
+
|
|
162
|
+
// Also get original for saturation analysis
|
|
163
|
+
const origBuf = await sharp(imgBuffer).raw().toBuffer({ resolveWithObject: true });
|
|
164
|
+
const { data: origData, info: origInfo } = origBuf;
|
|
165
|
+
|
|
166
|
+
if (origInfo.width !== width || origInfo.height !== height) {
|
|
167
|
+
throw new Error('Grayscale/Original dimension mismatch — should not happen');
|
|
168
|
+
}
|
|
169
|
+
|
|
151
170
|
const cellW = Math.floor(width / cols);
|
|
152
171
|
const cellH = Math.floor(height / rows);
|
|
153
172
|
|
|
@@ -159,65 +178,103 @@ async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
|
|
|
159
178
|
const ex = Math.min(sx + cellW, width);
|
|
160
179
|
const ey = Math.min(sy + cellH, height);
|
|
161
180
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
181
|
+
// Use 30% vertical padding to avoid Discord UI bar bleeding into top/bottom rows
|
|
182
|
+
const padX = Math.floor((ex - sx) * 0.20);
|
|
183
|
+
const padY = Math.floor((ey - sy) * 0.30);
|
|
184
|
+
const csx = sx + padX;
|
|
185
|
+
const csy = sy + padY;
|
|
186
|
+
const cex = ex - padX;
|
|
187
|
+
const cey = ey - padY;
|
|
169
188
|
|
|
170
189
|
let total = 0;
|
|
171
|
-
let
|
|
172
|
-
let
|
|
173
|
-
let
|
|
174
|
-
let
|
|
175
|
-
let
|
|
176
|
-
let
|
|
190
|
+
let harvestPx = 0;
|
|
191
|
+
let plantPx = 0;
|
|
192
|
+
let soilPx = 0;
|
|
193
|
+
let graySum = 0, graySqSum = 0;
|
|
194
|
+
let brightGrayPx = 0;
|
|
195
|
+
let darkGrayPx = 0;
|
|
196
|
+
let rSum = 0, gSum = 0, bSum = 0, satSum = 0;
|
|
197
|
+
let discordUiPx = 0;
|
|
198
|
+
const rawCellPixels = (cex - csx) * (cey - csy);
|
|
177
199
|
|
|
178
200
|
for (let y = csy; y < cey; y++) {
|
|
179
201
|
for (let x = csx; x < cex; x++) {
|
|
180
202
|
const idx = (y * width + x) * channels;
|
|
181
|
-
const r =
|
|
182
|
-
const g =
|
|
183
|
-
const b =
|
|
184
|
-
const
|
|
203
|
+
const r = origData[idx];
|
|
204
|
+
const g = origData[idx + 1];
|
|
205
|
+
const b = origData[idx + 2];
|
|
206
|
+
const gray = grayData[idx];
|
|
207
|
+
|
|
208
|
+
const maxC = Math.max(r, g, b);
|
|
209
|
+
const minC = Math.min(r, g, b);
|
|
210
|
+
const sat = maxC > 0 ? (maxC - minC) / 255 : 0;
|
|
211
|
+
|
|
212
|
+
// Skip Discord UI blue bar: very low saturation + blue dominant
|
|
213
|
+
if (b > 80 && sat < 0.08) {
|
|
214
|
+
discordUiPx++;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
185
218
|
total++;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (
|
|
219
|
+
graySum += gray;
|
|
220
|
+
graySqSum += gray * gray;
|
|
221
|
+
rSum += r; gSum += g; bSum += b;
|
|
222
|
+
satSum += sat;
|
|
223
|
+
|
|
224
|
+
if (g > r * 1.25) harvestPx++;
|
|
225
|
+
else if (g > r * 1.08) plantPx++;
|
|
226
|
+
if (r >= g) soilPx++;
|
|
227
|
+
if (gray > 100) brightGrayPx++;
|
|
228
|
+
if (gray < 65) darkGrayPx++;
|
|
192
229
|
}
|
|
193
230
|
}
|
|
194
231
|
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
const
|
|
232
|
+
const harvestPct = total > 0 ? harvestPx / total : 0;
|
|
233
|
+
const plantPct = total > 0 ? plantPx / total : 0;
|
|
234
|
+
const soilPct = total > 0 ? soilPx / total : 0;
|
|
235
|
+
const grayMean = total > 0 ? graySum / total : 0;
|
|
236
|
+
const grayVar = total > 0 ? (graySqSum / total) - (grayMean * grayMean) : 0;
|
|
237
|
+
const grayStd = Math.sqrt(Math.max(0, grayVar));
|
|
238
|
+
const avgSat = total > 0 ? satSum / total : 0;
|
|
239
|
+
const avgR = total > 0 ? rSum / total : 0;
|
|
240
|
+
const avgG = total > 0 ? gSum / total : 0;
|
|
241
|
+
const brightGrayPct = total > 0 ? brightGrayPx / total : 0;
|
|
242
|
+
const darkGrayPct = total > 0 ? darkGrayPx / total : 0;
|
|
243
|
+
const gMinusR = avgG - avgR;
|
|
244
|
+
const avgB = total > 0 ? bSum / total : 0;
|
|
245
|
+
const discordUiPct = rawCellPixels > 0 ? discordUiPx / rawCellPixels : 0;
|
|
246
|
+
|
|
247
|
+
const classified = classifyCell({
|
|
248
|
+
harvestPct, plantPct, soilPct,
|
|
249
|
+
avgR, avgG, avgB,
|
|
250
|
+
avgBrightness: grayMean,
|
|
251
|
+
avgSat, grayStd,
|
|
252
|
+
brightGrayPct, darkGrayPct,
|
|
253
|
+
gMinusR,
|
|
254
|
+
discordUiPct,
|
|
255
|
+
});
|
|
203
256
|
|
|
204
257
|
cells.push({
|
|
205
|
-
row,
|
|
206
|
-
col,
|
|
258
|
+
row, col,
|
|
207
259
|
state: classified.state,
|
|
208
260
|
confidence: classified.confidence,
|
|
209
261
|
scores: classified.scores,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
262
|
+
harvestPct: +harvestPct.toFixed(3),
|
|
263
|
+
plantPct: +plantPct.toFixed(3),
|
|
264
|
+
soilPct: +soilPct.toFixed(3),
|
|
265
|
+
avgBrightness: +grayMean.toFixed(1),
|
|
266
|
+
avgSat: +avgSat.toFixed(3),
|
|
267
|
+
grayStd: +grayStd.toFixed(1),
|
|
268
|
+
brightGrayPct: +brightGrayPct.toFixed(3),
|
|
269
|
+
darkGrayPct: +darkGrayPct.toFixed(3),
|
|
270
|
+
avgR: +avgR.toFixed(1),
|
|
271
|
+
avgG: +avgG.toFixed(1),
|
|
272
|
+
avgB: +avgB.toFixed(1),
|
|
216
273
|
});
|
|
217
274
|
}
|
|
218
275
|
}
|
|
219
276
|
|
|
220
|
-
const counts = {
|
|
277
|
+
const counts = { harvest: 0, water: 0, tilled: 0, planted: 0, unknown: 0 };
|
|
221
278
|
for (const c of cells) counts[c.state] = (counts[c.state] || 0) + 1;
|
|
222
279
|
|
|
223
280
|
const avgConfidence = cells.length > 0
|
|
@@ -228,7 +285,7 @@ async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
|
|
|
228
285
|
}
|
|
229
286
|
|
|
230
287
|
function gridToString(analysis) {
|
|
231
|
-
const icon = {
|
|
288
|
+
const icon = { harvest: 'H', water: 'W', tilled: 'T', planted: 'P', unknown: '?' };
|
|
232
289
|
const rows = [];
|
|
233
290
|
for (let r = 0; r < (analysis?.rows || 0); r++) {
|
|
234
291
|
const line = [];
|
|
@@ -243,93 +300,93 @@ function gridToString(analysis) {
|
|
|
243
300
|
|
|
244
301
|
function evaluateActionNeed(actionName, analysis) {
|
|
245
302
|
const c = analysis?.counts || {};
|
|
246
|
-
const
|
|
247
|
-
const wet = c.wet || 0;
|
|
303
|
+
const harvest = c.harvest || 0;
|
|
248
304
|
const planted = c.planted || 0;
|
|
305
|
+
const water = c.water || 0;
|
|
306
|
+
const tilled = (c.tilled || 0) + (c.soil || 0); // soil = tilled cells
|
|
249
307
|
const unknown = c.unknown || 0;
|
|
250
308
|
const conf = analysis?.avgConfidence || 0;
|
|
251
309
|
const slots = 9;
|
|
252
310
|
|
|
253
311
|
// Avoid aggressive repeats when confidence is too low.
|
|
254
|
-
if (conf < 0.
|
|
312
|
+
if (conf < 0.20) return false;
|
|
255
313
|
|
|
256
|
-
//
|
|
314
|
+
// Rules (4-state model: harvest/water/tilled/planted):
|
|
315
|
+
// - hoe: repeat if farm has no soil (all harvest/unknown)
|
|
316
|
+
// - water: repeat if farm has tilled but not watered (R<58, not water)
|
|
317
|
+
// - plant: repeat if farm has watered tiles but no planted
|
|
318
|
+
// - harvest: repeat if any harvest-ready crops visible
|
|
257
319
|
if (actionName === 'hoe') {
|
|
258
|
-
return
|
|
320
|
+
return harvest < slots && unknown < 6;
|
|
259
321
|
}
|
|
260
322
|
if (actionName === 'water') {
|
|
261
|
-
|
|
262
|
-
return tilled > 0 && (wet + planted) < slots;
|
|
323
|
+
return tilled > 0 && water < slots;
|
|
263
324
|
}
|
|
264
325
|
if (actionName === 'plant') {
|
|
265
|
-
return planted < slots && (
|
|
326
|
+
return planted < slots && (water + tilled) > 0;
|
|
266
327
|
}
|
|
267
328
|
if (actionName === 'harvest') {
|
|
268
|
-
|
|
269
|
-
return planted > 0;
|
|
329
|
+
return harvest > 0;
|
|
270
330
|
}
|
|
271
|
-
|
|
272
|
-
// Fertilizer optional; do not force repeats.
|
|
273
331
|
return false;
|
|
274
332
|
}
|
|
275
333
|
|
|
276
334
|
function evaluateActionScores(actionName, analysis) {
|
|
277
335
|
const c = analysis?.counts || {};
|
|
278
336
|
const slots = Math.max(1, (analysis?.rows || 3) * (analysis?.cols || 3));
|
|
279
|
-
const
|
|
280
|
-
const wet = c.wet || 0;
|
|
337
|
+
const harvest = c.harvest || 0;
|
|
281
338
|
const planted = c.planted || 0;
|
|
339
|
+
const water = c.water || 0;
|
|
340
|
+
const tilled = (c.tilled || 0) + (c.soil || 0); // soil = tilled cells
|
|
282
341
|
const unknown = c.unknown || 0;
|
|
283
342
|
const conf = analysis?.avgConfidence || 0;
|
|
284
343
|
|
|
285
344
|
const ratios = {
|
|
286
|
-
|
|
287
|
-
wet: wet / slots,
|
|
345
|
+
harvest: harvest / slots,
|
|
288
346
|
planted: planted / slots,
|
|
347
|
+
water: water / slots,
|
|
348
|
+
tilled: tilled / slots,
|
|
289
349
|
unknown: unknown / slots,
|
|
290
350
|
};
|
|
291
351
|
|
|
292
352
|
const clamp = (n) => Math.max(0, Math.min(1, n));
|
|
293
|
-
const withConf = (base
|
|
353
|
+
const withConf = (base) => clamp(base * (0.85 + conf * 0.3));
|
|
294
354
|
|
|
295
355
|
let score = 0;
|
|
296
|
-
let threshold = 0.
|
|
356
|
+
let threshold = 0.7;
|
|
297
357
|
let reason = 'ok';
|
|
298
358
|
|
|
299
359
|
if (actionName === 'hoe') {
|
|
300
|
-
// Hoe is
|
|
301
|
-
const base = (ratios.tilled * 0.
|
|
360
|
+
// Hoe is good if farm has mostly soil, not harvest-ready crops
|
|
361
|
+
const base = (ratios.tilled * 0.7) + (ratios.water * 0.7) + (ratios.planted * 0.5) - (ratios.harvest * 0.9) - (ratios.unknown * 0.3);
|
|
302
362
|
score = withConf(base);
|
|
303
|
-
threshold = 0.
|
|
304
|
-
if (ratios.unknown > 0.
|
|
305
|
-
else if (ratios.
|
|
363
|
+
threshold = 0.40;
|
|
364
|
+
if (ratios.unknown > 0.5) reason = 'too_many_unknown_cells';
|
|
365
|
+
else if (ratios.harvest >= 0.5) reason = 'farm_has_crops_to_harvest';
|
|
366
|
+
else if (ratios.harvest + ratios.planted < 0.3) reason = 'farm_needs_hoe';
|
|
306
367
|
} else if (actionName === 'water') {
|
|
307
|
-
//
|
|
308
|
-
const base = (ratios.
|
|
368
|
+
// Water is good if there are tilled (dry) tiles to water
|
|
369
|
+
const base = (ratios.tilled * 0.95) - (ratios.water * 0.5) - (ratios.harvest * 0.5) - (ratios.unknown * 0.2);
|
|
309
370
|
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';
|
|
371
|
+
threshold = 0.40;
|
|
372
|
+
if (ratios.harvest >= 0.5) reason = 'farm_has_crops_to_harvest';
|
|
373
|
+
else if (ratios.tilled < 0.3) reason = 'no_tilled_tiles';
|
|
318
374
|
} else if (actionName === 'plant') {
|
|
319
|
-
// Plant
|
|
320
|
-
const base = (ratios.planted * 0.
|
|
375
|
+
// Plant should end with growing crops on watered soil
|
|
376
|
+
const base = (ratios.planted * 0.95) - (ratios.harvest * 0.1) - (ratios.tilled * 0.2) - (ratios.unknown * 0.1);
|
|
321
377
|
score = withConf(base);
|
|
322
|
-
threshold = 0.
|
|
323
|
-
if (ratios.
|
|
378
|
+
threshold = 0.50;
|
|
379
|
+
if (ratios.harvest >= 0.5) reason = 'farm_ready_to_harvest';
|
|
380
|
+
else if (ratios.water + ratios.tilled < 0.3) reason = 'farm_not_watered';
|
|
324
381
|
} else if (actionName === 'harvest') {
|
|
325
|
-
// Harvest
|
|
326
|
-
const base = (
|
|
382
|
+
// Harvest is good if harvest-ready tiles dominate
|
|
383
|
+
const base = (ratios.harvest * 0.95) - (ratios.tilled * 0.15) - (ratios.planted * 0.1) - (ratios.unknown * 0.1);
|
|
327
384
|
score = withConf(base);
|
|
328
|
-
threshold = 0.
|
|
329
|
-
if (ratios.
|
|
385
|
+
threshold = 0.40;
|
|
386
|
+
if (ratios.harvest < 0.3) reason = 'no_crops_to_harvest';
|
|
330
387
|
} else {
|
|
331
|
-
score = withConf(0.
|
|
332
|
-
threshold = 0.
|
|
388
|
+
score = withConf(0.6 - ratios.unknown * 0.3);
|
|
389
|
+
threshold = 0.6;
|
|
333
390
|
}
|
|
334
391
|
|
|
335
392
|
const matched = conf >= 0.2 && score >= threshold;
|
|
@@ -341,11 +398,12 @@ function evaluateActionScores(actionName, analysis) {
|
|
|
341
398
|
threshold: +threshold.toFixed(3),
|
|
342
399
|
confidence: +conf.toFixed(3),
|
|
343
400
|
reason,
|
|
344
|
-
counts: {
|
|
401
|
+
counts: { harvest, planted, water, tilled, unknown, slots },
|
|
345
402
|
ratios: {
|
|
346
|
-
|
|
347
|
-
wet: +ratios.wet.toFixed(3),
|
|
403
|
+
harvest: +ratios.harvest.toFixed(3),
|
|
348
404
|
planted: +ratios.planted.toFixed(3),
|
|
405
|
+
water: +ratios.water.toFixed(3),
|
|
406
|
+
tilled: +ratios.tilled.toFixed(3),
|
|
349
407
|
unknown: +ratios.unknown.toFixed(3),
|
|
350
408
|
},
|
|
351
409
|
};
|
package/lib/commands/utils.js
CHANGED
|
@@ -494,9 +494,19 @@ async function ensureCV2(msg, force = false) {
|
|
|
494
494
|
if (rawParsed && rawParsed.components?.length > 0) {
|
|
495
495
|
msg._cv2 = rawParsed.components;
|
|
496
496
|
msg._cv2text = rawParsed.cv2Text || _extractCV2Text(rawParsed.components).trim();
|
|
497
|
-
|
|
497
|
+
const topLevel = rawParsed.buttons?.length > 0
|
|
498
498
|
? rawParsed.buttons.map(b => ({ type: 'BUTTON', label: b.label, customId: b.customId, style: b.style, url: null, disabled: b.disabled, emoji: b.emoji, _raw: b }))
|
|
499
|
-
:
|
|
499
|
+
: [];
|
|
500
|
+
const nested = _extractCV2Buttons(rawParsed.components);
|
|
501
|
+
// Merge, deduplicating by customId so all buttons are captured (top-level cv2Buttons
|
|
502
|
+
// omits some nested "All" buttons that live inside ACTION_ROW containers).
|
|
503
|
+
const seen = new Set();
|
|
504
|
+
const merged = [...topLevel, ...nested].filter(b => {
|
|
505
|
+
const id = b.customId || b.custom_id || '';
|
|
506
|
+
if (!id || seen.has(id)) return false;
|
|
507
|
+
seen.add(id); return true;
|
|
508
|
+
});
|
|
509
|
+
msg._cv2buttons = merged;
|
|
500
510
|
msg._cv2EditedTs = msgEditedTs;
|
|
501
511
|
cv2Cache.set(msg.id, { components: rawParsed.components, editedTimestamp: msgEditedTs });
|
|
502
512
|
return msg;
|
|
@@ -558,7 +568,7 @@ async function clickCV2Button(msg, customId) {
|
|
|
558
568
|
const gId = msg.guildId || msg.guild?.id;
|
|
559
569
|
if (!token) throw new Error('No token for CV2 click');
|
|
560
570
|
const sessionId = msg.client?.ws?.shards?.first?.()?.sessionId;
|
|
561
|
-
const nonce = `${BigInt(Date.now() - 1420070400000) << 22n}`;
|
|
571
|
+
const nonce = `${(BigInt(Date.now() - 1420070400000) << 22n)}`;
|
|
562
572
|
const payloadObj = {
|
|
563
573
|
type: 3,
|
|
564
574
|
application_id: String(msg.applicationId || DANK_MEMER_ID),
|
|
@@ -574,7 +584,11 @@ async function clickCV2Button(msg, customId) {
|
|
|
574
584
|
const resp = await _httpPost('https://discord.com/api/v9/interactions', {
|
|
575
585
|
Authorization: token, 'Content-Type': 'application/json',
|
|
576
586
|
}, payload);
|
|
577
|
-
if (resp.status >= 400)
|
|
587
|
+
if (resp.status >= 400) {
|
|
588
|
+
const errMsg = `CV2 click ${resp.status}: ${resp.body.substring(0, 300)}`;
|
|
589
|
+
LOG.warn(`[cv2] ${errMsg}`);
|
|
590
|
+
throw new Error(errMsg);
|
|
591
|
+
}
|
|
578
592
|
|
|
579
593
|
let parsed = null;
|
|
580
594
|
try {
|
package/lib/grinder.js
CHANGED
|
@@ -644,7 +644,11 @@ function renderDashboard() {
|
|
|
644
644
|
if (ls === 0) lsStr = `${R}♥0${c.reset}`;
|
|
645
645
|
else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${c.reset}`;
|
|
646
646
|
else if (ls != null) lsStr = `${G}♥${ls}${c.reset}`;
|
|
647
|
-
else
|
|
647
|
+
else {
|
|
648
|
+
// Unknown — pulse to show it's still being determined
|
|
649
|
+
const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
|
|
650
|
+
lsStr = `${D}${pulse}♥?${c.reset}`;
|
|
651
|
+
}
|
|
648
652
|
|
|
649
653
|
// ── Level indicator (fixed width so value changes don't jitter) ──
|
|
650
654
|
const lvl = wk._level || 0;
|
|
@@ -1839,9 +1843,15 @@ class AccountWorker {
|
|
|
1839
1843
|
}
|
|
1840
1844
|
}
|
|
1841
1845
|
|
|
1842
|
-
// ── Lifesaver protection: skip crime/search if 0 lifesavers ──
|
|
1846
|
+
// ── Lifesaver protection: skip crime/search if 0 lifesavers or unknown ──
|
|
1843
1847
|
if (cmdName === 'crime' || cmdName === 'search') {
|
|
1844
|
-
//
|
|
1848
|
+
// Unknown (null/undefined): DMs haven't confirmed safety — skip for safety.
|
|
1849
|
+
if (this._lifesavers == null) {
|
|
1850
|
+
this.log('warn', `[${cmdName}] SKIPPED — lifesavers unknown (safety hold)`);
|
|
1851
|
+
await this.setCooldown(cmdName, 600);
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
// Zero: depleted — disable for a long period.
|
|
1845
1855
|
if (this._lifesavers === 0) {
|
|
1846
1856
|
this.log('warn', `[${cmdName}] SKIPPED — 0 lifesavers (in-memory)`);
|
|
1847
1857
|
await this.setCooldown(cmdName, 3600);
|
|
@@ -3125,19 +3135,43 @@ async function start(apiKey, apiUrl) {
|
|
|
3125
3135
|
// Init rawLogger Redis (uses same URL — logs all raw gateway data)
|
|
3126
3136
|
if (REDIS_URL) {
|
|
3127
3137
|
rawLogger.init(REDIS_URL).catch(() => {});
|
|
3128
|
-
// Listen for DM
|
|
3138
|
+
// Listen for DM events across all accounts — update worker state + dashboard LIVE
|
|
3129
3139
|
rawLogger.onDmEvent((event, raw) => {
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3140
|
+
const channelId = raw.channel_id;
|
|
3141
|
+
for (const w of workers) {
|
|
3142
|
+
const isThisWorker = w.client?.user?.dmChannel?.id === channelId;
|
|
3143
|
+
if (!isThisWorker && w.channel?.id !== channelId) continue;
|
|
3144
|
+
|
|
3145
|
+
if (event.type === 'death') {
|
|
3146
|
+
// Update worker's lifesaver count so dashboard ♥ updates in real time
|
|
3147
|
+
if (event.lifesaversLeft >= 0) {
|
|
3148
|
+
const prev = w._lifesavers;
|
|
3149
|
+
w._lifesavers = event.lifesaversLeft;
|
|
3150
|
+
if (event.lifesaversLeft === 0) {
|
|
3151
|
+
w.log?.('error', `DEATH in DMs! 0 lifesavers — disabling crime/search`);
|
|
3152
|
+
w.setCooldown?.('crime', 86400);
|
|
3153
|
+
w.setCooldown?.('search', 86400);
|
|
3154
|
+
sendWebhook?.('DEATH ALERT (DM)', `**${w.username}** died in DMs! **0 lifesavers!**\nCrime/search auto-disabled.`, 0xef4444);
|
|
3155
|
+
} else {
|
|
3156
|
+
w.log?.('warn', `DEATH in DMs — ${event.lifesaversLeft} lifesavers remaining`);
|
|
3157
|
+
if (prev !== event.lifesaversLeft) {
|
|
3158
|
+
w.setCooldown?.('crime', 60);
|
|
3159
|
+
w.setCooldown?.('search', 60);
|
|
3160
|
+
}
|
|
3161
|
+
if (event.lifesaversLeft <= 2) {
|
|
3162
|
+
sendWebhook?.('LOW LIFESAVERS', `**${w.username}** has only **${event.lifesaversLeft}** lifesaver(s) left!`, 0xfbbf24);
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
scheduleRender();
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
if (event.type === 'levelup') {
|
|
3170
|
+
if (event.to > 0) {
|
|
3171
|
+
w._level = event.to;
|
|
3172
|
+
scheduleRender();
|
|
3138
3173
|
}
|
|
3139
3174
|
}
|
|
3140
|
-
sendWebhook?.('DEATH ALERT (DM)', `Account died! **0 lifesavers!**\nCrime/search auto-disabled.`, 0xef4444);
|
|
3141
3175
|
}
|
|
3142
3176
|
});
|
|
3143
3177
|
checks.push(`${rgb(52, 211, 153)}✓${c.reset} ${c.white}RawLog${c.reset}`);
|
|
@@ -3457,14 +3491,16 @@ async function start(apiKey, apiUrl) {
|
|
|
3457
3491
|
|
|
3458
3492
|
|
|
3459
3493
|
// Phase 2.75: Check DM history for deaths/level-ups (sequential, fast)
|
|
3460
|
-
|
|
3461
|
-
|
|
3494
|
+
const dmCheckPulse = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
|
|
3495
|
+
console.log(` ${rgb(139, 92, 246)}${dmCheckPulse}${c.reset} ${c.dim}Checking DM history...${c.reset}`);
|
|
3496
|
+
let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [], dmUnknown = [];
|
|
3462
3497
|
for (const w of activeWorkers) {
|
|
3463
3498
|
try {
|
|
3464
3499
|
const dm = await w.checkDmHistory();
|
|
3465
3500
|
if (dm.deaths > 0) dmDeaths += dm.deaths;
|
|
3466
3501
|
if (dm.levelUps > 0) dmLevelUps += dm.levelUps;
|
|
3467
3502
|
if (dm.lifesavers === 0) dmNoLs.push(w.username);
|
|
3503
|
+
if (dm.lifesavers === -1) dmUnknown.push(w.username);
|
|
3468
3504
|
// Store level and lifesaver for dashboard
|
|
3469
3505
|
if (dm.currentLevel > 0) w._level = dm.currentLevel;
|
|
3470
3506
|
if (dm.lifesavers >= 0) w._lifesavers = dm.lifesavers;
|
|
@@ -3474,6 +3510,10 @@ async function start(apiKey, apiUrl) {
|
|
|
3474
3510
|
if (dm.lifesavers >= 0) {
|
|
3475
3511
|
const lc = dm.lifesavers === 0 ? rgb(239, 68, 68) : dm.lifesavers <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
|
|
3476
3512
|
parts.push(`${lc}♥${dm.lifesavers}${c.reset}`);
|
|
3513
|
+
} else {
|
|
3514
|
+
// Unknown lifesavers — pulse to show pending
|
|
3515
|
+
const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
|
|
3516
|
+
parts.push(`${D}${pulse}♥?${c.reset}`);
|
|
3477
3517
|
}
|
|
3478
3518
|
if (parts.length > 0) {
|
|
3479
3519
|
console.log(` ${c.dim}├${c.reset} ${c.bold}${w.username}${c.reset} ${parts.join(' ')}`);
|
|
@@ -3492,7 +3532,16 @@ async function start(apiKey, apiUrl) {
|
|
|
3492
3532
|
}
|
|
3493
3533
|
}
|
|
3494
3534
|
}
|
|
3495
|
-
|
|
3535
|
+
if (dmUnknown.length > 0) {
|
|
3536
|
+
console.log(` ${rgb(251, 191, 36)}⚠${c.reset} ${c.dim}Lifesavers unknown — live DM monitor active:${c.reset} ${dmUnknown.join(', ')}`);
|
|
3537
|
+
// Crime/search on these accounts will be skipped via safety hold until the live
|
|
3538
|
+
// DM gateway listener detects a death (→ sets count) or confirms clean.
|
|
3539
|
+
}
|
|
3540
|
+
const dmSummaryParts = [];
|
|
3541
|
+
if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
|
|
3542
|
+
if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
|
|
3543
|
+
if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
|
|
3544
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}DM check${c.reset} ${dmSummaryParts.length > 0 ? c.dim + dmSummaryParts.join(', ') + c.reset : c.dim + 'clean — no deaths or level-ups' + c.reset}`);
|
|
3496
3545
|
console.log('');
|
|
3497
3546
|
|
|
3498
3547
|
console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
|