dankgrinder 4.8.1 → 4.9.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.
@@ -5,33 +5,51 @@
5
5
 
6
6
  const {
7
7
  LOG, c, getFullText, parseCoins, getAllButtons, safeClickButton,
8
- logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
8
+ logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, ensureCV2,
9
9
  } = require('./utils');
10
10
 
11
- function parsePlayerTotal(text) {
12
- // Match the player section: "username (Player)\n J 2 12"
13
- // The number after the cards on the player line is the total
14
- const playerSection = text.match(/\(Player\)[\s\S]*?(\d+)/i);
15
- if (playerSection) return parseInt(playerSection[1]);
16
- const totalMatch = text.match(/total[:\s]*\**(\d+)\**/i) || text.match(/value[:\s]*\**(\d+)\**/i);
17
- return totalMatch ? parseInt(totalMatch[1]) : 0;
11
+ function parsePlayerTotal(msg) {
12
+ // Score is in embed field: ` 18 ` (backtick-wrapped number in Player field)
13
+ for (const embed of msg?.embeds || []) {
14
+ for (const field of embed.fields || []) {
15
+ if (field.name?.toLowerCase().includes('player')) {
16
+ const m = field.value.match(/`\s*(\d+)\s*`/);
17
+ if (m) return parseInt(m[1]);
18
+ }
19
+ }
20
+ }
21
+ const text = typeof msg === 'string' ? msg : getFullText(msg);
22
+ const m = text.match(/`\s*(\d+)\s*`/);
23
+ return m ? parseInt(m[1]) : 0;
18
24
  }
19
25
 
20
- function parseDealerUpcard(text) {
21
- // Match dealer section: "Dank Memer (Dealer)\n 10 8 18"
22
- const dealerSection = text.match(/\(Dealer\)[\s\S]*?(\d+)/i);
23
- if (dealerSection) return parseInt(dealerSection[1]);
26
+ function parseDealerUpcard(msg) {
27
+ // Dealer field: revealed total ` 18 ` or hidden ` ? `
28
+ // Upcard from emoji: bjFaceKB=K(10), bjFaceAR=A(11), bjFace7R=7
29
+ for (const embed of msg?.embeds || []) {
30
+ for (const field of embed.fields || []) {
31
+ if (field.name?.toLowerCase().includes('dealer')) {
32
+ const totalMatch = field.value.match(/`\s*(\d+)\s*`/);
33
+ if (totalMatch) return parseInt(totalMatch[1]);
34
+ const faces = [...field.value.matchAll(/bjFace(\w+?):/g)];
35
+ for (const [, face] of faces) {
36
+ if (face === 'Unknown') continue;
37
+ const v = face.replace(/[RB]$/, '');
38
+ if (v === 'A') return 11;
39
+ if (['K', 'Q', 'J'].includes(v)) return 10;
40
+ const n = parseInt(v);
41
+ if (!isNaN(n)) return n;
42
+ }
43
+ }
44
+ }
45
+ }
24
46
  return 0;
25
47
  }
26
48
 
27
49
  function parseNetCoins(text) {
28
- // Parse "Net: ⏣ -5,000" or "Net: 5,000"
29
- const netMatch = text.match(/Net:\s*[⏣o]\s*(-?[\d,]+)/i);
30
- if (netMatch) {
31
- return parseInt(netMatch[1].replace(/,/g, ''));
32
- }
33
- // Parse "Winnings: ⏣ 5,000"
34
- const winMatch = text.match(/Winnings:\s*[⏣o]\s*([\d,]+)/i);
50
+ const netMatch = text.match(/Net:\s*\*{0,2}\s*[]\s*\*{0,2}([+-]?[\d,]+)/i);
51
+ if (netMatch) return parseInt(netMatch[1].replace(/,/g, ''));
52
+ const winMatch = text.match(/Winnings:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([\d,]+)/i);
35
53
  if (winMatch) return parseInt(winMatch[1].replace(/,/g, ''));
36
54
  return 0;
37
55
  }
@@ -63,6 +81,8 @@ async function runBlackjack({ channel, waitForDankMemer, betAmount = 5000 }) {
63
81
  return { result: 'no response', coins: 0 };
64
82
  }
65
83
 
84
+ await ensureCV2(current);
85
+
66
86
  if (isHoldTight(current)) {
67
87
  const reason = getHoldTightReason(current);
68
88
  LOG.warn(`[bj] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
@@ -74,25 +94,26 @@ async function runBlackjack({ channel, waitForDankMemer, betAmount = 5000 }) {
74
94
 
75
95
  const MAX_ROUNDS = 10;
76
96
  for (let i = 0; i < MAX_ROUNDS; i++) {
77
- const text = getFullText(current);
78
- const buttons = getAllButtons(current);
79
- if (buttons.length === 0 || buttons.every(b => b.disabled)) break;
97
+ const buttons = getAllButtons(current).filter(b => b.label && b.label !== 'null');
98
+ const hitBtn = buttons.find(b => !b.disabled && (b.label || '').toLowerCase().includes('hit'));
99
+ const standBtn = buttons.find(b => !b.disabled && (b.label || '').toLowerCase().includes('stand'));
100
+ if (!hitBtn && !standBtn) break;
80
101
 
81
- const playerTotal = parsePlayerTotal(text);
82
- const dealerTotal = parseDealerUpcard(text);
102
+ const playerTotal = parsePlayerTotal(current);
103
+ const dealerUpcard = parseDealerUpcard(current);
83
104
 
84
105
  let targetBtn;
85
106
  if (playerTotal === 0) {
86
- targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('stand')) || buttons[1];
87
- } else if (shouldHit(playerTotal, dealerTotal)) {
88
- targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('hit')) || buttons[0];
107
+ targetBtn = standBtn || hitBtn;
108
+ } else if (shouldHit(playerTotal, dealerUpcard)) {
109
+ targetBtn = hitBtn || standBtn;
89
110
  } else {
90
- targetBtn = buttons.find(b => (b.label || '').toLowerCase().includes('stand')) || buttons[1];
111
+ targetBtn = standBtn || hitBtn;
91
112
  }
92
113
 
93
- if (!targetBtn || targetBtn.disabled) break;
114
+ if (!targetBtn) break;
94
115
 
95
- LOG.info(`[bj] You:${playerTotal} Dealer:${dealerTotal} → ${targetBtn.label}`);
116
+ LOG.info(`[bj] You:${playerTotal} Dealer:${dealerUpcard} → ${targetBtn.label}`);
96
117
  await humanDelay(400, 900);
97
118
 
98
119
  try {
@@ -100,8 +121,6 @@ async function runBlackjack({ channel, waitForDankMemer, betAmount = 5000 }) {
100
121
  if (followUp) {
101
122
  current = followUp;
102
123
  logMsg(current, `bj-round-${i}`);
103
- const fButtons = getAllButtons(current);
104
- if (fButtons.length === 0 || fButtons.every(b => b.disabled)) break;
105
124
  } else break;
106
125
  } catch { break; }
107
126
  }
@@ -1,17 +1,16 @@
1
1
  /**
2
2
  * Gambling command handlers.
3
- * Covers: cointoss, roulette, slots, snakeeyes
3
+ * Covers: cointoss (CV2), roulette, slots, snakeeyes
4
+ * Each handler is tailored to the actual Dank Memer response format.
4
5
  */
5
6
 
6
7
  const {
7
8
  LOG, c, getFullText, parseCoins, parseNetCoins, getAllButtons, safeClickButton,
8
- logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay,
9
+ logMsg, isHoldTight, getHoldTightReason, sleep, humanDelay, ensureCV2,
9
10
  } = require('./utils');
10
11
 
11
- function parseGambleResult(text, cmdName) {
12
+ function parseResult(text, cmdName) {
12
13
  const net = parseNetCoins(text);
13
- const lower = text.toLowerCase();
14
-
15
14
  if (net > 0) {
16
15
  LOG.coin(`[${cmdName}] ${c.green}+⏣ ${net.toLocaleString()}${c.reset}`);
17
16
  return { result: `${cmdName} → +⏣ ${net.toLocaleString()}`, coins: net };
@@ -25,84 +24,188 @@ function parseGambleResult(text, cmdName) {
25
24
  LOG.coin(`[${cmdName}] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
26
25
  return { result: `${cmdName} → +⏣ ${coins.toLocaleString()}`, coins };
27
26
  }
28
- if (lower.includes('won') || lower.includes('beat')) return { result: `${cmdName} → won`, coins: 0 };
29
- if (lower.includes('lost') || lower.includes('bust')) return { result: `${cmdName} → lost`, coins: 0, lost: coins };
30
-
31
27
  return { result: `${cmdName} done`, coins: 0 };
32
28
  }
33
29
 
34
- async function runGamble({ channel, waitForDankMemer, cmdName, cmdString }) {
35
- LOG.cmd(`${c.white}${c.bold}${cmdString}${c.reset}`);
30
+ function checkMinBet(text) {
31
+ const lower = text.toLowerCase();
32
+ if (lower.includes("can't bet less than") || lower.includes('cannot bet less than') || lower.includes('minimum bet')) {
33
+ const m = text.match(/less than\s*\*?\*?[⏣o]?\s*([\d,]+)/i) || text.match(/(\d[\d,]+)/);
34
+ if (m) return parseInt(m[1].replace(/,/g, ''));
35
+ return -1;
36
+ }
37
+ return 0;
38
+ }
36
39
 
37
- await channel.send(cmdString);
38
- const response = await waitForDankMemer(10000);
40
+ // Filter utility buttons (null labels, warnings, refresh)
41
+ function gameButtons(msg) {
42
+ return getAllButtons(msg).filter(b =>
43
+ !b.disabled && b.label && b.label !== 'null' &&
44
+ !(b.customId || b.custom_id || '').includes('warning') &&
45
+ !(b.customId || b.custom_id || '').includes('refresh')
46
+ );
47
+ }
39
48
 
49
+ async function commonChecks(response, cmdName) {
40
50
  if (!response) {
41
51
  LOG.warn(`[${cmdName}] No response`);
42
- return { result: 'no response', coins: 0 };
52
+ return { skip: true, ret: { result: 'no response', coins: 0 } };
43
53
  }
44
-
45
54
  if (isHoldTight(response)) {
46
55
  const reason = getHoldTightReason(response);
47
- LOG.warn(`[${cmdName}] Hold Tight${reason ? ` (reason: /${reason})` : ''} — waiting 30s`);
56
+ LOG.warn(`[${cmdName}] Hold Tight${reason ? ` (/${reason})` : ''} — waiting 30s`);
48
57
  await sleep(30000);
49
- return { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason };
58
+ return { skip: true, ret: { result: `hold tight (${reason || 'unknown'})`, coins: 0, holdTightReason: reason } };
59
+ }
60
+ const text = getFullText(response);
61
+ const minBet = checkMinBet(text);
62
+ if (minBet !== 0) {
63
+ const val = minBet > 0 ? minBet : 0;
64
+ LOG.warn(`[${cmdName}] Min bet: ⏣ ${val > 0 ? val.toLocaleString() : '?'}`);
65
+ return { skip: true, ret: { result: `min bet ${val}`, coins: 0, newMinBet: val > 0 ? val : undefined } };
50
66
  }
67
+ return { skip: false };
68
+ }
51
69
 
52
- // Check for min bet error
53
- const initText = getFullText(response);
54
- const initLower = initText.toLowerCase();
55
- if (initLower.includes("can't bet less than") || initLower.includes('cannot bet less than') || initLower.includes('minimum bet')) {
56
- const betMatch = initText.match(/less than\s*\*?\*?[⏣o]?\s*([\d,]+)/i) || initText.match(/(\d[\d,]+)/);
57
- if (betMatch) {
58
- const minBet = parseInt(betMatch[1].replace(/,/g, ''));
59
- if (minBet > 0) return { result: `min bet ${minBet}`, coins: 0, newMinBet: minBet };
60
- }
61
- return { result: 'min bet error', coins: 0 };
70
+ /**
71
+ * Cointoss — CV2 format. Fetch raw components, pick Heads/Tails randomly.
72
+ */
73
+ async function runCointoss({ channel, waitForDankMemer, betAmount = 10000 }) {
74
+ const cmd = `pls cointoss ${betAmount}`;
75
+ LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
76
+ await channel.send(cmd);
77
+ let response = await waitForDankMemer(10000);
78
+
79
+ await ensureCV2(response);
80
+ const chk = await commonChecks(response, 'cointoss');
81
+ if (chk.skip) return chk.ret;
82
+
83
+ logMsg(response, 'cointoss');
84
+ const btns = gameButtons(response).filter(b =>
85
+ !(b.customId || b.custom_id || '').includes('bet')
86
+ );
87
+
88
+ if (btns.length > 0) {
89
+ const choice = Math.random() < 0.5 ? 'head' : 'tail';
90
+ const btn = btns.find(b => (b.label || '').toLowerCase().includes(choice)) || btns[0];
91
+ LOG.info(`[cointoss] Picking "${btn.label}"`);
92
+ await humanDelay(200, 500);
93
+ try {
94
+ const followUp = await safeClickButton(response, btn);
95
+ if (followUp) {
96
+ await ensureCV2(followUp);
97
+ return parseResult(getFullText(followUp), 'cointoss');
98
+ }
99
+ // CV2 click returns null — wait for message update, clear cache, refetch
100
+ await sleep(2500);
101
+ response._cv2 = null;
102
+ response._cv2text = null;
103
+ response._cv2buttons = null;
104
+ await ensureCV2(response);
105
+ return parseResult(getFullText(response), 'cointoss');
106
+ } catch (e) { LOG.error(`[cointoss] Click error: ${e.message}`); }
62
107
  }
63
108
 
64
- logMsg(response, cmdName);
65
- const text = getFullText(response);
109
+ return parseResult(getFullText(response), 'cointoss');
110
+ }
66
111
 
67
- // For cointoss/gambles with buttons: click a random non-disabled button
68
- const buttons = getAllButtons(response);
69
- if (buttons.length > 0) {
70
- const clickable = buttons.filter(b => !b.disabled);
71
- if (clickable.length > 0) {
72
- const btn = clickable[Math.floor(Math.random() * clickable.length)];
73
- LOG.info(`[${cmdName}] Clicking "${btn.label || '?'}"`);
74
- await humanDelay(50, 200);
75
- try {
76
- const followUp = await safeClickButton(response, btn);
77
- if (followUp) {
78
- logMsg(followUp, `${cmdName}-result`);
79
- const fText = getFullText(followUp);
80
- return parseGambleResult(fText, cmdName);
81
- }
82
- } catch (e) { LOG.error(`[${cmdName}] Click error: ${e.message}`); }
83
- }
112
+ /**
113
+ * Roulette — embed format. Always pick Red (best even-money odds ~48.6%).
114
+ * Sometimes auto-bets if color is in command, sometimes shows buttons.
115
+ */
116
+ async function runRoulette({ channel, waitForDankMemer, betAmount = 10000 }) {
117
+ const cmd = `pls roulette ${betAmount} red`;
118
+ LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
119
+ await channel.send(cmd);
120
+ let response = await waitForDankMemer(10000);
121
+
122
+ const chk = await commonChecks(response, 'roulette');
123
+ if (chk.skip) return chk.ret;
124
+
125
+ logMsg(response, 'roulette');
126
+
127
+ // Check if buttons appeared (manual pick needed)
128
+ const btns = gameButtons(response).filter(b =>
129
+ !(b.customId || b.custom_id || '').includes('bet')
130
+ );
131
+ const redBtn = btns.find(b => (b.label || '').toLowerCase() === 'red');
132
+ if (redBtn) {
133
+ LOG.info(`[roulette] Picking "Red"`);
134
+ await humanDelay(200, 500);
135
+ try {
136
+ const followUp = await safeClickButton(response, redBtn);
137
+ if (followUp) {
138
+ response = followUp;
139
+ logMsg(response, 'roulette-after-pick');
140
+ }
141
+ } catch (e) { LOG.error(`[roulette] Click error: ${e.message}`); }
84
142
  }
85
143
 
86
- return parseGambleResult(text, cmdName);
144
+ // Wait for spin animation to complete
145
+ let text = getFullText(response);
146
+ if (text.toLowerCase().includes('spinning') || text.toLowerCase().includes('rolling') || text.toLowerCase().includes('pick a color')) {
147
+ await sleep(3500);
148
+ try {
149
+ const final = await channel.messages.fetch(response.id);
150
+ if (final) text = getFullText(final);
151
+ } catch {}
152
+ }
153
+
154
+ return parseResult(text, 'roulette');
87
155
  }
88
156
 
89
157
  /**
90
- * Cointoss: send "pls cointoss <bet>", then click Heads or Tails randomly.
158
+ * Slots embed format. Auto-spins, no button needed. Wait for animation.
91
159
  */
92
- async function runCointoss({ channel, waitForDankMemer, betAmount = 10000 }) {
93
- return runGamble({ channel, waitForDankMemer, cmdName: 'cointoss', cmdString: `pls cointoss ${betAmount}` });
94
- }
160
+ async function runSlots({ channel, waitForDankMemer, betAmount = 10000 }) {
161
+ const cmd = `pls slots ${betAmount}`;
162
+ LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
163
+ await channel.send(cmd);
164
+ let response = await waitForDankMemer(10000);
95
165
 
96
- async function runRoulette({ channel, waitForDankMemer, betAmount = 5000 }) {
97
- return runGamble({ channel, waitForDankMemer, cmdName: 'roulette', cmdString: `pls roulette ${betAmount} red` });
98
- }
166
+ const chk = await commonChecks(response, 'slots');
167
+ if (chk.skip) return chk.ret;
168
+
169
+ logMsg(response, 'slots');
170
+ const text = getFullText(response);
171
+
172
+ // Slots auto-spins — if still animating, wait and refetch
173
+ if (text.toLowerCase().includes('spinning')) {
174
+ await sleep(3500);
175
+ try {
176
+ const final = await channel.messages.fetch(response.id);
177
+ if (final) return parseResult(getFullText(final), 'slots');
178
+ } catch {}
179
+ }
99
180
 
100
- async function runSlots({ channel, waitForDankMemer, betAmount = 5000 }) {
101
- return runGamble({ channel, waitForDankMemer, cmdName: 'slots', cmdString: `pls slots ${betAmount}` });
181
+ return parseResult(text, 'slots');
102
182
  }
103
183
 
104
- async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 5000 }) {
105
- return runGamble({ channel, waitForDankMemer, cmdName: 'snakeeyes', cmdString: `pls snakeeyes ${betAmount}` });
184
+ /**
185
+ * Snakeeyes embed format. Auto-rolls, no button needed. Wait for animation.
186
+ */
187
+ async function runSnakeeyes({ channel, waitForDankMemer, betAmount = 10000 }) {
188
+ const cmd = `pls snakeeyes ${betAmount}`;
189
+ LOG.cmd(`${c.white}${c.bold}${cmd}${c.reset}`);
190
+ await channel.send(cmd);
191
+ let response = await waitForDankMemer(10000);
192
+
193
+ const chk = await commonChecks(response, 'snakeeyes');
194
+ if (chk.skip) return chk.ret;
195
+
196
+ logMsg(response, 'snakeeyes');
197
+ const text = getFullText(response);
198
+
199
+ // Snakeeyes auto-rolls — if still animating, wait and refetch
200
+ if (text.toLowerCase().includes('rolling')) {
201
+ await sleep(3500);
202
+ try {
203
+ const final = await channel.messages.fetch(response.id);
204
+ if (final) return parseResult(getFullText(final), 'snakeeyes');
205
+ } catch {}
206
+ }
207
+
208
+ return parseResult(text, 'snakeeyes');
106
209
  }
107
210
 
108
- module.exports = { runGamble, runCointoss, runRoulette, runSlots, runSnakeeyes };
211
+ module.exports = { runCointoss, runRoulette, runSlots, runSnakeeyes };
@@ -23,7 +23,7 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
23
23
  LOG.cmd(`${c.white}${c.bold}${cmdString}${c.reset}`);
24
24
 
25
25
  await channel.send(cmdString);
26
- const response = await waitForDankMemer(10000);
26
+ let response = await waitForDankMemer(10000);
27
27
 
28
28
  if (!response) {
29
29
  LOG.warn(`[${cmdName}] No response`);
@@ -109,12 +109,14 @@ async function runGeneric({ channel, waitForDankMemer, cmdString, cmdName, clien
109
109
  // Find row index of first select menu
110
110
  let menuRowIdx = -1;
111
111
  for (let i = 0; i < (response.components || []).length; i++) {
112
- for (const comp of response.components[i].components || []) {
112
+ const row = response.components[i];
113
+ if (!row) continue;
114
+ for (const comp of row.components || []) {
113
115
  if (comp.type === 'STRING_SELECT' || comp.type === 3) { menuRowIdx = i; break; }
114
116
  }
115
117
  if (menuRowIdx >= 0) break;
116
118
  }
117
- const menu = menuRowIdx >= 0 ? response.components[menuRowIdx].components[0] : null;
119
+ const menu = menuRowIdx >= 0 ? response.components?.[menuRowIdx]?.components?.[0] : null;
118
120
  const options = menu?.options || [];
119
121
  if (options.length > 0 && menuRowIdx >= 0) {
120
122
  const opt = options[Math.floor(Math.random() * options.length)];
@@ -27,9 +27,9 @@ function parseHintNumber(text) {
27
27
  }
28
28
 
29
29
  function parseNetCoins(text) {
30
- const netMatch = text.match(/Net:\s*[⏣o]\s*(-?[\d,]+)/i);
30
+ const netMatch = text.match(/Net:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([+-]?[\d,]+)/i);
31
31
  if (netMatch) return parseInt(netMatch[1].replace(/,/g, ''));
32
- const winMatch = text.match(/Winnings:\s*[⏣o]\s*([\d,]+)/i);
32
+ const winMatch = text.match(/Winnings:\s*\*{0,2}\s*[⏣]\s*\*{0,2}([\d,]+)/i);
33
33
  if (winMatch) return parseInt(winMatch[1].replace(/,/g, ''));
34
34
  return 0;
35
35
  }
@@ -16,13 +16,14 @@ const { runScratch } = require('./scratch');
16
16
  const { runBlackjack } = require('./blackjack');
17
17
  const { runTrivia, triviaDB } = require('./trivia');
18
18
  const { runWorkShift } = require('./work');
19
- const { runCointoss, runRoulette, runSlots, runSnakeeyes, runGamble } = require('./gamble');
19
+ const { runCointoss, runRoulette, runSlots, runSnakeeyes } = require('./gamble');
20
20
  const { runDeposit } = require('./deposit');
21
21
  const { runGeneric, runAlert } = require('./generic');
22
22
  const { runStream } = require('./stream');
23
23
  const { runDrops } = require('./drops');
24
24
  const { buyItem, ITEM_COSTS } = require('./shop');
25
25
  const { getPlayerLevel, meetsLevelRequirement } = require('./profile');
26
+ const { runInventory, fetchItemValues, enrichItems, getCachedInventory, getAllInventories, updateInventoryItem, deleteInventoryItem } = require('./inventory');
26
27
 
27
28
  module.exports = {
28
29
  // Individual commands
@@ -43,7 +44,6 @@ module.exports = {
43
44
  runRoulette,
44
45
  runSlots,
45
46
  runSnakeeyes,
46
- runGamble,
47
47
  runDeposit,
48
48
  runGeneric,
49
49
  runAlert,
@@ -51,6 +51,15 @@ module.exports = {
51
51
  runDrops,
52
52
  buyItem,
53
53
 
54
+ // Inventory
55
+ runInventory,
56
+ fetchItemValues,
57
+ enrichItems,
58
+ getCachedInventory,
59
+ getAllInventories,
60
+ updateInventoryItem,
61
+ deleteInventoryItem,
62
+
54
63
  // Profile / Level
55
64
  getPlayerLevel,
56
65
  meetsLevelRequirement,