dankgrinder 7.6.0 → 7.11.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/bin/dankgrinder.js +2 -2
- package/lib/commands/farm.js +416 -86
- package/lib/commands/farmVision.js +190 -96
- package/lib/commands/utils.js +17 -3
- package/lib/grinder.js +77 -20
- package/lib/rawLogger.js +23 -2
- package/package.json +1 -1
|
@@ -82,16 +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
|
-
|
|
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;
|
|
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;
|
|
93
88
|
const whiteish = brightness > 210 && Math.abs(r - g) < 20 && Math.abs(g - b) < 20;
|
|
94
|
-
return { brightness,
|
|
89
|
+
return { brightness, gray, soil, whiteish };
|
|
95
90
|
}
|
|
96
91
|
|
|
97
92
|
function clamp01(n) {
|
|
@@ -99,41 +94,79 @@ function clamp01(n) {
|
|
|
99
94
|
}
|
|
100
95
|
|
|
101
96
|
function classifyCell(features) {
|
|
102
|
-
const {
|
|
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 } };
|
|
108
|
+
}
|
|
103
109
|
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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) } };
|
|
109
130
|
}
|
|
110
131
|
|
|
111
|
-
// ──
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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) } };
|
|
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) } };
|
|
118
136
|
}
|
|
119
137
|
|
|
120
|
-
// ──
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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) } };
|
|
126
148
|
}
|
|
127
149
|
|
|
128
|
-
// ── UNKNOWN: Low signal
|
|
129
|
-
const signal = Math.max(
|
|
150
|
+
// ── UNKNOWN: Low signal / empty ──
|
|
151
|
+
const signal = Math.max(harvestPct, plantPct, soilPct, avgSat);
|
|
130
152
|
const confidence = signal * 1.5;
|
|
131
|
-
return { state: 'unknown', confidence: +confidence.toFixed(3), scores: {
|
|
153
|
+
return { state: 'unknown', confidence: +confidence.toFixed(3), scores: { harvest: +harvestPct.toFixed(3), planted: +plantPct.toFixed(3), soil: +soilPct.toFixed(3) } };
|
|
132
154
|
}
|
|
133
155
|
|
|
134
156
|
async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
|
|
135
|
-
|
|
136
|
-
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
|
+
|
|
137
170
|
const cellW = Math.floor(width / cols);
|
|
138
171
|
const cellH = Math.floor(height / rows);
|
|
139
172
|
|
|
@@ -145,60 +178,103 @@ async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
|
|
|
145
178
|
const ex = Math.min(sx + cellW, width);
|
|
146
179
|
const ey = Math.min(sy + cellH, height);
|
|
147
180
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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;
|
|
155
188
|
|
|
156
189
|
let total = 0;
|
|
157
|
-
let
|
|
158
|
-
let
|
|
159
|
-
let
|
|
160
|
-
let
|
|
161
|
-
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);
|
|
162
199
|
|
|
163
200
|
for (let y = csy; y < cey; y++) {
|
|
164
201
|
for (let x = csx; x < cex; x++) {
|
|
165
202
|
const idx = (y * width + x) * channels;
|
|
166
|
-
const r =
|
|
167
|
-
const g =
|
|
168
|
-
const b =
|
|
169
|
-
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
|
+
|
|
170
218
|
total++;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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++;
|
|
176
229
|
}
|
|
177
230
|
}
|
|
178
231
|
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
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
|
+
});
|
|
186
256
|
|
|
187
257
|
cells.push({
|
|
188
|
-
row,
|
|
189
|
-
col,
|
|
258
|
+
row, col,
|
|
190
259
|
state: classified.state,
|
|
191
260
|
confidence: classified.confidence,
|
|
192
261
|
scores: classified.scores,
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
avgBrightness: +
|
|
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),
|
|
197
273
|
});
|
|
198
274
|
}
|
|
199
275
|
}
|
|
200
276
|
|
|
201
|
-
const counts = { tilled: 0, planted: 0, unknown: 0 };
|
|
277
|
+
const counts = { harvest: 0, water: 0, tilled: 0, planted: 0, unknown: 0 };
|
|
202
278
|
for (const c of cells) counts[c.state] = (counts[c.state] || 0) + 1;
|
|
203
279
|
|
|
204
280
|
const avgConfidence = cells.length > 0
|
|
@@ -209,7 +285,7 @@ async function analyzeFarmGrid(imgBuffer, cols = 3, rows = 3) {
|
|
|
209
285
|
}
|
|
210
286
|
|
|
211
287
|
function gridToString(analysis) {
|
|
212
|
-
const icon = { tilled: 'T', planted: 'P', unknown: '?' };
|
|
288
|
+
const icon = { harvest: 'H', water: 'W', tilled: 'T', planted: 'P', unknown: '?' };
|
|
213
289
|
const rows = [];
|
|
214
290
|
for (let r = 0; r < (analysis?.rows || 0); r++) {
|
|
215
291
|
const line = [];
|
|
@@ -224,9 +300,10 @@ function gridToString(analysis) {
|
|
|
224
300
|
|
|
225
301
|
function evaluateActionNeed(actionName, analysis) {
|
|
226
302
|
const c = analysis?.counts || {};
|
|
227
|
-
const
|
|
228
|
-
const wet = c.wet || 0;
|
|
303
|
+
const harvest = c.harvest || 0;
|
|
229
304
|
const planted = c.planted || 0;
|
|
305
|
+
const water = c.water || 0;
|
|
306
|
+
const tilled = (c.tilled || 0) + (c.soil || 0); // soil = tilled cells
|
|
230
307
|
const unknown = c.unknown || 0;
|
|
231
308
|
const conf = analysis?.avgConfidence || 0;
|
|
232
309
|
const slots = 9;
|
|
@@ -234,35 +311,41 @@ function evaluateActionNeed(actionName, analysis) {
|
|
|
234
311
|
// Avoid aggressive repeats when confidence is too low.
|
|
235
312
|
if (conf < 0.20) return false;
|
|
236
313
|
|
|
237
|
-
//
|
|
238
|
-
// - hoe: repeat
|
|
239
|
-
// - water:
|
|
240
|
-
// - plant: repeat
|
|
241
|
-
// - harvest: repeat if any
|
|
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
|
|
242
319
|
if (actionName === 'hoe') {
|
|
243
|
-
return
|
|
320
|
+
return harvest < slots && unknown < 6;
|
|
321
|
+
}
|
|
322
|
+
if (actionName === 'water') {
|
|
323
|
+
return tilled > 0 && water < slots;
|
|
244
324
|
}
|
|
245
325
|
if (actionName === 'plant') {
|
|
246
|
-
return planted < slots && (tilled) > 0;
|
|
326
|
+
return planted < slots && (water + tilled) > 0;
|
|
247
327
|
}
|
|
248
328
|
if (actionName === 'harvest') {
|
|
249
|
-
return
|
|
329
|
+
return harvest > 0;
|
|
250
330
|
}
|
|
251
|
-
// water and others: don't force repeats
|
|
252
331
|
return false;
|
|
253
332
|
}
|
|
254
333
|
|
|
255
334
|
function evaluateActionScores(actionName, analysis) {
|
|
256
335
|
const c = analysis?.counts || {};
|
|
257
336
|
const slots = Math.max(1, (analysis?.rows || 3) * (analysis?.cols || 3));
|
|
258
|
-
const
|
|
337
|
+
const harvest = c.harvest || 0;
|
|
259
338
|
const planted = c.planted || 0;
|
|
339
|
+
const water = c.water || 0;
|
|
340
|
+
const tilled = (c.tilled || 0) + (c.soil || 0); // soil = tilled cells
|
|
260
341
|
const unknown = c.unknown || 0;
|
|
261
342
|
const conf = analysis?.avgConfidence || 0;
|
|
262
343
|
|
|
263
344
|
const ratios = {
|
|
264
|
-
|
|
345
|
+
harvest: harvest / slots,
|
|
265
346
|
planted: planted / slots,
|
|
347
|
+
water: water / slots,
|
|
348
|
+
tilled: tilled / slots,
|
|
266
349
|
unknown: unknown / slots,
|
|
267
350
|
};
|
|
268
351
|
|
|
@@ -274,24 +357,33 @@ function evaluateActionScores(actionName, analysis) {
|
|
|
274
357
|
let reason = 'ok';
|
|
275
358
|
|
|
276
359
|
if (actionName === 'hoe') {
|
|
277
|
-
// Hoe is good if farm has mostly soil
|
|
278
|
-
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);
|
|
279
362
|
score = withConf(base);
|
|
280
|
-
threshold = 0.
|
|
363
|
+
threshold = 0.40;
|
|
281
364
|
if (ratios.unknown > 0.5) reason = 'too_many_unknown_cells';
|
|
282
|
-
else if (ratios.
|
|
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';
|
|
367
|
+
} else if (actionName === 'water') {
|
|
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);
|
|
370
|
+
score = withConf(base);
|
|
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';
|
|
283
374
|
} else if (actionName === 'plant') {
|
|
284
|
-
// Plant should end with
|
|
285
|
-
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);
|
|
286
377
|
score = withConf(base);
|
|
287
|
-
threshold = 0.
|
|
288
|
-
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';
|
|
289
381
|
} else if (actionName === 'harvest') {
|
|
290
|
-
// Harvest is good if
|
|
291
|
-
const base = (ratios.
|
|
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);
|
|
292
384
|
score = withConf(base);
|
|
293
|
-
threshold = 0.
|
|
294
|
-
if (ratios.
|
|
385
|
+
threshold = 0.40;
|
|
386
|
+
if (ratios.harvest < 0.3) reason = 'no_crops_to_harvest';
|
|
295
387
|
} else {
|
|
296
388
|
score = withConf(0.6 - ratios.unknown * 0.3);
|
|
297
389
|
threshold = 0.6;
|
|
@@ -306,10 +398,12 @@ function evaluateActionScores(actionName, analysis) {
|
|
|
306
398
|
threshold: +threshold.toFixed(3),
|
|
307
399
|
confidence: +conf.toFixed(3),
|
|
308
400
|
reason,
|
|
309
|
-
counts: {
|
|
401
|
+
counts: { harvest, planted, water, tilled, unknown, slots },
|
|
310
402
|
ratios: {
|
|
311
|
-
|
|
403
|
+
harvest: +ratios.harvest.toFixed(3),
|
|
312
404
|
planted: +ratios.planted.toFixed(3),
|
|
405
|
+
water: +ratios.water.toFixed(3),
|
|
406
|
+
tilled: +ratios.tilled.toFixed(3),
|
|
313
407
|
unknown: +ratios.unknown.toFixed(3),
|
|
314
408
|
},
|
|
315
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;
|
|
@@ -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}`);
|
|
@@ -3180,10 +3214,12 @@ async function start(apiKey, apiUrl) {
|
|
|
3180
3214
|
loginLines.push(` ${'─'.repeat(loginVis)}`);
|
|
3181
3215
|
for (const l of loginLines) console.log(l);
|
|
3182
3216
|
|
|
3183
|
-
// Dynamically capture the starting row of the login table via DSR
|
|
3217
|
+
// Dynamically capture the starting row of the login table via DSR.
|
|
3218
|
+
// Write MARKER to stderr (not stdout) to avoid PTY cooked-mode echoing
|
|
3219
|
+
// of the visible "@MARKER@@" text portion, which was causing the DSR
|
|
3220
|
+
// response to be swallowed or delayed.
|
|
3184
3221
|
let loginBaseRow = 1;
|
|
3185
3222
|
const captureLoginRow = () => new Promise(resolve => {
|
|
3186
|
-
process.stdout.write(MARKER);
|
|
3187
3223
|
const chunks = [];
|
|
3188
3224
|
const handler = (chunk) => {
|
|
3189
3225
|
chunks.push(chunk);
|
|
@@ -3196,6 +3232,8 @@ async function start(apiKey, apiUrl) {
|
|
|
3196
3232
|
}
|
|
3197
3233
|
};
|
|
3198
3234
|
process.stdin.on('data', handler);
|
|
3235
|
+
// Write to stderr so PTY doesn't echo the visible MARKER text to stdout
|
|
3236
|
+
process.stderr.write(MARKER);
|
|
3199
3237
|
setTimeout(resolve, 50);
|
|
3200
3238
|
});
|
|
3201
3239
|
await captureLoginRow();
|
|
@@ -3297,7 +3335,7 @@ async function start(apiKey, apiUrl) {
|
|
|
3297
3335
|
const invVis = 7 + iColNum + iColName + iColItems + iColVal + 12;
|
|
3298
3336
|
|
|
3299
3337
|
// Print a unique marker, query its position, then overwrite it with the table
|
|
3300
|
-
|
|
3338
|
+
// Set up stdin handler BEFORE writing MARKER (same fix as Phase 1 — avoids race)
|
|
3301
3339
|
let invBaseRow = 1;
|
|
3302
3340
|
const captureRow = () => new Promise(resolve => {
|
|
3303
3341
|
const chunks = [];
|
|
@@ -3312,6 +3350,8 @@ async function start(apiKey, apiUrl) {
|
|
|
3312
3350
|
}
|
|
3313
3351
|
};
|
|
3314
3352
|
process.stdin.on('data', handler);
|
|
3353
|
+
// Write to stderr so PTY doesn't echo the visible MARKER text to stdout
|
|
3354
|
+
process.stderr.write(MARKER);
|
|
3315
3355
|
setTimeout(resolve, 50);
|
|
3316
3356
|
});
|
|
3317
3357
|
await captureRow();
|
|
@@ -3380,7 +3420,7 @@ async function start(apiKey, apiUrl) {
|
|
|
3380
3420
|
const balVis = 7 + bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 14;
|
|
3381
3421
|
|
|
3382
3422
|
// Capture starting row for balance phase
|
|
3383
|
-
|
|
3423
|
+
// Set up stdin handler BEFORE writing MARKER (same fix — avoids race + PTY echo)
|
|
3384
3424
|
let balBaseRow = 1;
|
|
3385
3425
|
const balCaptureRow = () => new Promise(resolve => {
|
|
3386
3426
|
const chunks = [];
|
|
@@ -3395,6 +3435,8 @@ async function start(apiKey, apiUrl) {
|
|
|
3395
3435
|
}
|
|
3396
3436
|
};
|
|
3397
3437
|
process.stdin.on('data', handler);
|
|
3438
|
+
// Write to stderr so PTY doesn't echo the visible MARKER text to stdout
|
|
3439
|
+
process.stderr.write(MARKER);
|
|
3398
3440
|
setTimeout(resolve, 50);
|
|
3399
3441
|
});
|
|
3400
3442
|
await balCaptureRow();
|
|
@@ -3457,14 +3499,16 @@ async function start(apiKey, apiUrl) {
|
|
|
3457
3499
|
|
|
3458
3500
|
|
|
3459
3501
|
// Phase 2.75: Check DM history for deaths/level-ups (sequential, fast)
|
|
3460
|
-
|
|
3461
|
-
|
|
3502
|
+
const dmCheckPulse = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
|
|
3503
|
+
console.log(` ${rgb(139, 92, 246)}${dmCheckPulse}${c.reset} ${c.dim}Checking DM history...${c.reset}`);
|
|
3504
|
+
let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [], dmUnknown = [];
|
|
3462
3505
|
for (const w of activeWorkers) {
|
|
3463
3506
|
try {
|
|
3464
3507
|
const dm = await w.checkDmHistory();
|
|
3465
3508
|
if (dm.deaths > 0) dmDeaths += dm.deaths;
|
|
3466
3509
|
if (dm.levelUps > 0) dmLevelUps += dm.levelUps;
|
|
3467
3510
|
if (dm.lifesavers === 0) dmNoLs.push(w.username);
|
|
3511
|
+
if (dm.lifesavers === -1) dmUnknown.push(w.username);
|
|
3468
3512
|
// Store level and lifesaver for dashboard
|
|
3469
3513
|
if (dm.currentLevel > 0) w._level = dm.currentLevel;
|
|
3470
3514
|
if (dm.lifesavers >= 0) w._lifesavers = dm.lifesavers;
|
|
@@ -3474,6 +3518,10 @@ async function start(apiKey, apiUrl) {
|
|
|
3474
3518
|
if (dm.lifesavers >= 0) {
|
|
3475
3519
|
const lc = dm.lifesavers === 0 ? rgb(239, 68, 68) : dm.lifesavers <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
|
|
3476
3520
|
parts.push(`${lc}♥${dm.lifesavers}${c.reset}`);
|
|
3521
|
+
} else {
|
|
3522
|
+
// Unknown lifesavers — pulse to show pending
|
|
3523
|
+
const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
|
|
3524
|
+
parts.push(`${D}${pulse}♥?${c.reset}`);
|
|
3477
3525
|
}
|
|
3478
3526
|
if (parts.length > 0) {
|
|
3479
3527
|
console.log(` ${c.dim}├${c.reset} ${c.bold}${w.username}${c.reset} ${parts.join(' ')}`);
|
|
@@ -3492,7 +3540,16 @@ async function start(apiKey, apiUrl) {
|
|
|
3492
3540
|
}
|
|
3493
3541
|
}
|
|
3494
3542
|
}
|
|
3495
|
-
|
|
3543
|
+
if (dmUnknown.length > 0) {
|
|
3544
|
+
console.log(` ${rgb(251, 191, 36)}⚠${c.reset} ${c.dim}Lifesavers unknown — live DM monitor active:${c.reset} ${dmUnknown.join(', ')}`);
|
|
3545
|
+
// Crime/search on these accounts will be skipped via safety hold until the live
|
|
3546
|
+
// DM gateway listener detects a death (→ sets count) or confirms clean.
|
|
3547
|
+
}
|
|
3548
|
+
const dmSummaryParts = [];
|
|
3549
|
+
if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
|
|
3550
|
+
if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
|
|
3551
|
+
if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
|
|
3552
|
+
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
3553
|
console.log('');
|
|
3497
3554
|
|
|
3498
3555
|
console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
|