dankgrinder 8.109.0 → 8.110.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.
@@ -108,14 +108,31 @@ const SAFE_KEYWORDS = Object.freeze(['flee', 'run', 'hide', 'avoid', 'ignore', '
108
108
  const RISKY_KEYWORDS = Object.freeze(['reach', 'grab', 'fight', 'attack', 'steal', 'open', 'touch', 'eat', 'drink']);
109
109
  const ADVENTURE_PREFERRED_TYPES = Object.freeze(['space', 'out west']);
110
110
 
111
- function pickSafeChoice(choices) {
111
+ function pickSafeChoice(choices, adventureAnswers) {
112
112
  if (choices.length === 0) return null;
113
113
  if (choices.length === 1) return choices[0];
114
114
 
115
- // Check labels for safe/risky keywords
116
115
  const labels = choices.map(b => (b.label || '').toLowerCase());
117
116
 
118
- // Prefer safe keywords
117
+ // ── 1) Config answer maps (dmg pattern — highest priority) ──
118
+ if (adventureAnswers) {
119
+ for (const [question, answers] of Object.entries(adventureAnswers)) {
120
+ const matched = labels.find(l => l.includes(question));
121
+ if (matched && Array.isArray(answers)) {
122
+ for (const answer of answers) {
123
+ const found = choices.find(c =>
124
+ (c.label || '').toLowerCase().includes(answer.toLowerCase())
125
+ );
126
+ if (found) {
127
+ LOG.info(`[adventure] Config answer: "${found.label}" matched question "${question}"`);
128
+ return found;
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ // ── 2) Hardcoded safe keywords ──
119
136
  for (let i = 0; i < labels.length; i++) {
120
137
  for (let k = 0; k < SAFE_KEYWORDS.length; k++) {
121
138
  const kw = SAFE_KEYWORDS[k];
@@ -126,7 +143,7 @@ function pickSafeChoice(choices) {
126
143
  }
127
144
  }
128
145
 
129
- // Avoid risky keywords
146
+ // ── 3) Avoid risky keywords ──
130
147
  const nonRisky = choices.filter((b, i) => {
131
148
  return !RISKY_KEYWORDS.some(kw => labels[i].includes(kw));
132
149
  });
@@ -143,7 +160,7 @@ function pickSafeChoice(choices) {
143
160
  }
144
161
 
145
162
  // ── Play through all adventure rounds ────────────────────────────
146
- async function playAdventureRounds(channel, msg) {
163
+ async function playAdventureRounds(channel, msg, adventureAnswers) {
147
164
  let current = msg;
148
165
  let interactions = 0;
149
166
  const MAX_INTERACTIONS = 30;
@@ -171,7 +188,7 @@ async function playAdventureRounds(channel, msg) {
171
188
  // ── If there are choices, pick one first ─────────────────
172
189
  if (choices.length > 0) {
173
190
  LOG.info(`[adventure] ${choices.length} choices: [${choices.map(b => `"${b.label}"`).join(', ')}]`);
174
- const choice = pickSafeChoice(choices);
191
+ const choice = pickSafeChoice(choices, adventureAnswers);
175
192
  if (choice) {
176
193
  LOG.info(`[adventure] → Choosing: "${choice.label}"`);
177
194
  await sleep(50);
@@ -258,9 +275,10 @@ async function playAdventureRounds(channel, msg) {
258
275
  * @param {object} opts.channel - Discord channel
259
276
  * @param {function} opts.waitForDankMemer - Waits for Dank Memer response
260
277
  * @param {object} [opts.client] - Discord client (for modal handling in shop)
278
+ * @param {object} [opts.adventureAnswers] - Per-type answer map from AccountWorker.ADVENTURE_ANSWERS
261
279
  * @returns {Promise<{result: string, coins: number, nextCooldownSec: number|null}>}
262
280
  */
263
- async function runAdventure({ channel, waitForDankMemer, client }) {
281
+ async function runAdventure({ channel, waitForDankMemer, client, adventureAnswers }) {
264
282
  LOG.cmd(`${c.white}${c.bold}pls adventure${c.reset}`);
265
283
 
266
284
  // Step 1: Send the command
@@ -334,7 +352,7 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
334
352
  if (hasNextBtn && menus.length === 0) {
335
353
  // Already mid-adventure from a previous run — jump straight to rounds
336
354
  LOG.info('[adventure] Resuming mid-adventure...');
337
- const { text: finalText, coins, interactions, rewards, finalMsg } = await playAdventureRounds(channel, response);
355
+ const { text: finalText, coins, interactions, rewards, finalMsg } = await playAdventureRounds(channel, response, adventureAnswers);
338
356
  return buildResult(finalText, coins, interactions, rewards, finalMsg);
339
357
  }
340
358
 
@@ -434,7 +452,7 @@ async function runAdventure({ channel, waitForDankMemer, client }) {
434
452
  }
435
453
 
436
454
  // ── Play through all adventure rounds ──────────────────────
437
- const { text: finalText, coins, interactions, rewards, finalMsg } = await playAdventureRounds(channel, response);
455
+ const { text: finalText, coins, interactions, rewards, finalMsg } = await playAdventureRounds(channel, response, adventureAnswers);
438
456
  return buildResult(finalText, coins, interactions, rewards, finalMsg);
439
457
  }
440
458
 
package/lib/grinder.js CHANGED
@@ -598,6 +598,37 @@ class MinHeap {
598
598
 
599
599
  get size() { return this.heap.length; }
600
600
 
601
+ // ── Message handler registration (dmg pattern) ──────────────
602
+ // Allows plugins/commands to register handlers for gateway events.
603
+ // handlers: { messageCreate: Map<name, fn>, messageUpdate: Map<name, fn> }
604
+ // ── Dispatch events to registered handlers (dmg pattern) ──
605
+ _dispatchToHandlers(event, msg) {
606
+ const handlers = this._handlers[event];
607
+ if (!handlers || handlers.size === 0) return;
608
+ for (const [name, fn] of handlers) {
609
+ try {
610
+ const result = fn(msg);
611
+ if (result && typeof result.then === 'function') {
612
+ result.catch(e => this.log('error', `[handler:${name}] ${e.message}`));
613
+ }
614
+ } catch (e) {
615
+ this.log('error', `[handler:${name}] ${e.message}`);
616
+ }
617
+ }
618
+ }
619
+
620
+ _registerHandler(event, name, fn) {
621
+ if (this._handlers[event]) {
622
+ this._handlers[event].set(name, fn);
623
+ }
624
+ }
625
+
626
+ _unregisterHandler(event, name) {
627
+ if (this._handlers[event]) {
628
+ this._handlers[event].delete(name);
629
+ }
630
+ }
631
+
601
632
  _bubbleUp(i) {
602
633
  while (i > 0) {
603
634
  const parent = (i - 1) >> 1;
@@ -677,6 +708,14 @@ class AccountWorker {
677
708
  this._commandRunning = false; // prevents grinding commands from overlapping with quest commands
678
709
  this._verifyLevelUnlock = null; // holds level to verify after quest completion
679
710
  this.commandQueue = null;
711
+
712
+ // ── Message handler routing (dmg pattern) ─────────────────
713
+ // Map of event → handler functions. Plugins register here.
714
+ // Usage: _handlers.messageCreate.get('adventure')?.(msg)
715
+ this._handlers = {
716
+ messageCreate: new Map(),
717
+ messageUpdate: new Map(),
718
+ };
680
719
  this.lastHealthCheck = Date.now();
681
720
  this.doneToday = new Map();
682
721
 
@@ -762,6 +801,8 @@ class AccountWorker {
762
801
  }
763
802
  function handler(msg) {
764
803
  if (msg.author.id === DANK_MEMER_ID && msg.channel.id === self.channel.id) {
804
+ // ── Dispatch to registered handlers (dmg pattern) ────
805
+ self._dispatchToHandlers('messageCreate', msg);
765
806
  const hasComponentPayload = Array.isArray(msg.components)
766
807
  && msg.components.some(c => c && (c.components || c.content || c.type || c.label || c.customId));
767
808
  const hasContent = (msg.content && msg.content.length > 0)
@@ -791,6 +832,8 @@ class AccountWorker {
791
832
  // Reject edits to messages created well before our command was sent
792
833
  const msgTs = newMsg.createdTimestamp || newMsg.createdAt?.getTime?.() || 0;
793
834
  if (msgTs > 0 && msgTs < sentAt - 1500) return;
835
+ // ── Dispatch to registered handlers ─────────────────
836
+ self._dispatchToHandlers('messageUpdate', newMsg);
794
837
  cleanup();
795
838
  resolve(newMsg);
796
839
  }
@@ -1356,6 +1399,7 @@ class AccountWorker {
1356
1399
  client: this.client,
1357
1400
  safeAnswers: cmdName === 'search' ? safeParseJSON(this.account.search_answers, []) :
1358
1401
  cmdName === 'crime' ? safeParseJSON(this.account.crime_answers, []) : [],
1402
+ adventureAnswers: AccountWorker.ADVENTURE_ANSWERS,
1359
1403
  betAmount: this._questBetOverride || (['blackjack'].includes(cmdName) ? bjBet : gambBet),
1360
1404
  accountId: this.account.id,
1361
1405
  redis,
@@ -1821,6 +1865,42 @@ class AccountWorker {
1821
1865
  ].map(Object.freeze);
1822
1866
 
1823
1867
  // ── Level Quest System — blocks grinding until quests complete ──
1868
+ // ── Adventure answer maps (per type) ──────────────────────────
1869
+ // Pattern: question text → answer choice to click
1870
+ // Supports: space, out west, brazil, vacation, halloween, museum
1871
+ static ADVENTURE_ANSWERS = {
1872
+ space: {
1873
+ 'what do you want to do': ['search for debris', 'scavenge wreckage'],
1874
+ 'how do you respond': ['stay calm', 'analyze situation'],
1875
+ 'what do you choose': ['try to communicate', 'observe first'],
1876
+ },
1877
+ west: {
1878
+ 'what do you do': ['draw first', 'hide behind cover'],
1879
+ 'how do you respond': ['draw', 'shoot'],
1880
+ 'sheriff wants': ['accept bounty', 'decline'],
1881
+ },
1882
+ brazil: {
1883
+ 'what do you want to do': ['search for treasure', 'ask locals'],
1884
+ 'how do you respond': ['negotiate', 'accept offer'],
1885
+ 'locals offer': ['take the deal', 'refuse'],
1886
+ },
1887
+ vacation: {
1888
+ 'stranger offers': ['accept', 'decline'],
1889
+ 'what do you do': ['explore', 'rest'],
1890
+ 'something suspicious': ['investigate', 'ignore'],
1891
+ },
1892
+ halloween: {
1893
+ 'knock at door': ['open it', 'hide'],
1894
+ 'what do you do': ['investigate', 'run away'],
1895
+ 'ghost appears': ['speak to it', 'hide'],
1896
+ },
1897
+ museum: {
1898
+ 'what do you do': ['touch exhibit', 'keep distance'],
1899
+ 'alarm sounds': ['hide', 'run'],
1900
+ 'guard asks': ['tell truth', 'lie'],
1901
+ },
1902
+ };
1903
+
1824
1904
  static LEVEL_QUESTS = {
1825
1905
  1: [
1826
1906
  { cmd: 'beg', times: 2 },
package/lib/rawLogger.js CHANGED
@@ -457,6 +457,20 @@ async function store(d, event) {
457
457
  pipe.ltrim(accKey, 0, 4999);
458
458
  pipe.expire(accKey, LOG_TTL);
459
459
  }
460
+ // Track ephemeral in memory for quick access before they vanish
461
+ if (parsed.isEphemeral || (d.flags & 32832)) {
462
+ let chMap = ephemeralByChannel.get(d.channel_id);
463
+ if (!chMap) {
464
+ chMap = new Map();
465
+ ephemeralByChannel.set(d.channel_id, chMap);
466
+ }
467
+ chMap.set(d.id, parsed);
468
+ // Cap per-channel ephemeral buffer at 50
469
+ if (chMap.size > 50) {
470
+ const firstKey = chMap.keys().next().value;
471
+ chMap.delete(firstKey);
472
+ }
473
+ }
460
474
  // Ephemeral log
461
475
  if (parsed.isEphemeral || (d.flags & 32832)) {
462
476
  pipe.lpush('raw:ephemeral:log', `${d.id}:${parsed._capturedAt}:${parsed.command}:${d.channel_id}`);
@@ -494,8 +508,17 @@ function onDmEvent(fn) { dmListeners.push(fn); }
494
508
 
495
509
  // ── Ephemeral message callbacks ──
496
510
  const ephemeralListeners = [];
511
+ const ephemeralByChannel = new Map(); // channelId → Map<msgId, parsed msg>
512
+
497
513
  function onNextEphemeral(fn) { ephemeralListeners.push(fn); }
498
514
 
515
+ // Track ephemeral messages per channel — useful for reading state before vanish
516
+ function getEphemeralMsgs(channelId, count = 20) {
517
+ const map = ephemeralByChannel.get(channelId);
518
+ if (!map) return [];
519
+ return [...map.values()].slice(-count);
520
+ }
521
+
499
522
  function _notifyEphemeral(parsed) {
500
523
  if (!ephemeralListeners.length) return;
501
524
  const listeners = [...ephemeralListeners];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.109.0",
3
+ "version": "8.110.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"