@m3rcena/eco 0.0.0-stage → 14.28.1

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/DOCUMENTATION.md +869 -0
  3. package/LICENSE +191 -0
  4. package/README.md +182 -2
  5. package/assets/logo.png +0 -0
  6. package/dist/host-Bmld-WtZ.cjs +6163 -0
  7. package/dist/host-Bmld-WtZ.cjs.map +1 -0
  8. package/dist/host-XZmTzNyx.d.cts +1833 -0
  9. package/dist/host-XZmTzNyx.d.cts.map +1 -0
  10. package/dist/host-_xxHiG2p.d.mts +1833 -0
  11. package/dist/host-_xxHiG2p.d.mts.map +1 -0
  12. package/dist/host-qJNVzdDa.mjs +5894 -0
  13. package/dist/host-qJNVzdDa.mjs.map +1 -0
  14. package/dist/host.cjs +4 -0
  15. package/dist/host.d.cts +2 -0
  16. package/dist/host.d.mts +2 -0
  17. package/dist/host.mjs +2 -0
  18. package/dist/index.cjs +1440 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.cts +339 -0
  21. package/dist/index.d.cts.map +1 -0
  22. package/dist/index.d.mts +339 -0
  23. package/dist/index.d.mts.map +1 -0
  24. package/dist/index.mjs +1371 -0
  25. package/dist/index.mjs.map +1 -0
  26. package/dist/register.cjs +46 -0
  27. package/dist/register.cjs.map +1 -0
  28. package/dist/register.d.cts +6 -0
  29. package/dist/register.d.cts.map +1 -0
  30. package/dist/register.d.mts +6 -0
  31. package/dist/register.d.mts.map +1 -0
  32. package/dist/register.mjs +44 -0
  33. package/dist/register.mjs.map +1 -0
  34. package/dist/resolver.cjs +305 -0
  35. package/dist/resolver.cjs.map +1 -0
  36. package/dist/resolver.d.cts +40 -0
  37. package/dist/resolver.d.cts.map +1 -0
  38. package/dist/resolver.d.mts +40 -0
  39. package/dist/resolver.d.mts.map +1 -0
  40. package/dist/resolver.mjs +296 -0
  41. package/dist/resolver.mjs.map +1 -0
  42. package/dist/rolldown-runtime-CmQyd0Gu.cjs +27 -0
  43. package/dist/rolldown-runtime-D3PmRqjj.mjs +4 -0
  44. package/package.json +94 -4
package/dist/index.mjs ADDED
@@ -0,0 +1,1371 @@
1
+ import { A as DEFAULT_SHOP_ITEMS, B as applyPrismaEcoModel, C as HeistManager, D as CasinoManager, E as LeaderboardManager, F as BankManager, H as JsonEcoStorage, I as EcoDatabaseHandler, L as resolveEcoStorage, M as CareerManager, N as DEFAULT_JOBS, O as BusinessManager, P as RewardManager, R as PRISMA_ECO_MODEL, S as DEFAULT_HEIST_TARGETS, T as StockMarketManager, U as MemoryEcoStorage, V as SqliteEcoStorage, W as createDefaultAccount, _ as VaultManager, a as ECO_VERSION, b as DEFAULT_PETS, c as compareSemver, d as RenderMarketChart, f as RenderBankCard, g as DEFAULT_VAULT_TIERS, h as LoanManager, i as EcoBridgeRouter, j as ShopManager, k as DEFAULT_BUSINESS_CATALOG, l as fetchLatestVersion, m as LOAN_CATALOG, n as EcoManager, o as boxConsole, p as TaxManager, r as EcoWekyConnector, s as checkPackageUpdates, t as hostEco, u as stripAnsi, v as CRATE_PRICES, w as DEFAULT_STOCKS, x as PetManager, y as CrateManager, z as PrismaEcoStorage } from "./host-qJNVzdDa.mjs";
2
+ import { findProjectRoot, generateTsConfigPaths, getAllPackageResolutions, isTypeScriptRuntime, loadResolverConfig, printResolutionSummary, resolvePackage } from "./resolver.mjs";
3
+ import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, ContainerBuilder, GuildMember, Message, MessageFlags, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, User, resolveColor } from "discord.js";
4
+ import { M3rcenaBridge } from "@m3rcena/bridge";
5
+ // src/ui/ContextAdapter.ts
6
+ /**
7
+ * @m3rcena/eco
8
+ * Discord Context Adapter for Unified Slash Command, Message, and Dashboard Execution.
9
+ *
10
+ * @copyright 2026 M3rcena Development. All rights reserved.
11
+ * @license Apache-2.0
12
+ */
13
+ var ContextAdapter = class ContextAdapter {
14
+ raw;
15
+ user;
16
+ userId;
17
+ member;
18
+ guild;
19
+ channel;
20
+ client;
21
+ isInteraction;
22
+ isMessage;
23
+ gameMessage = null;
24
+ constructor(context) {
25
+ if (context instanceof ContextAdapter) context = context.raw;
26
+ this.raw = context;
27
+ if (context instanceof Message) {
28
+ this.isMessage = true;
29
+ this.isInteraction = false;
30
+ this.user = context.author;
31
+ this.userId = context.author.id;
32
+ this.member = context.member ?? null;
33
+ this.guild = context.guild ?? null;
34
+ this.channel = context.channel;
35
+ this.client = context.client;
36
+ } else if (typeof context?.reply === "function") {
37
+ this.isInteraction = true;
38
+ this.isMessage = false;
39
+ this.user = context.user || context.author;
40
+ this.userId = this.user?.id || context.member?.id;
41
+ this.member = context.member ?? null;
42
+ this.guild = context.guild ?? null;
43
+ this.channel = context.channel;
44
+ this.client = context.client;
45
+ } else {
46
+ this.isInteraction = false;
47
+ this.isMessage = true;
48
+ this.user = context.user || context.author;
49
+ this.userId = this.user?.id || context.member?.id;
50
+ this.member = context.member ?? null;
51
+ this.guild = context.guild ?? null;
52
+ this.channel = context.channel || (typeof context.send === "function" ? context : null);
53
+ this.client = context.client;
54
+ }
55
+ if (!this.user || !this.channel) throw new Error("[ContextAdapter] Invalid Discord context: unable to resolve user or channel.");
56
+ }
57
+ static from(context) {
58
+ return new ContextAdapter(context);
59
+ }
60
+ resolveFlags(options) {
61
+ let flags = typeof options.flags === "number" ? options.flags : 0;
62
+ if (options.components?.some((c) => c instanceof ContainerBuilder || typeof c?.setAccentColor === "function" || c?.data?.type === 17)) flags |= MessageFlags.IsComponentsV2;
63
+ return flags > 0 ? flags : void 0;
64
+ }
65
+ async sendInitial(options) {
66
+ if (typeof this.raw?.sendInitial === "function") {
67
+ const msg = await this.raw.sendInitial(options);
68
+ this.gameMessage = msg;
69
+ return msg;
70
+ }
71
+ const resolvedFlags = this.resolveFlags(options);
72
+ const isV2 = !!((resolvedFlags ?? 0) & MessageFlags.IsComponentsV2);
73
+ if (this.isInteraction) {
74
+ const interaction = this.raw;
75
+ if (interaction.deferred || interaction.replied) {
76
+ const payload = {
77
+ components: options.components,
78
+ files: options.files,
79
+ flags: resolvedFlags
80
+ };
81
+ if (!isV2) {
82
+ if (options.content !== void 0) payload.content = options.content;
83
+ if (options.embeds !== void 0) payload.embeds = options.embeds;
84
+ }
85
+ const msg = await interaction.editReply(payload);
86
+ if (msg instanceof Message) this.gameMessage = msg;
87
+ else if (typeof interaction.fetchReply === "function") this.gameMessage = await interaction.fetchReply().catch(() => null);
88
+ else this.gameMessage = msg || null;
89
+ } else {
90
+ const payload = {
91
+ components: options.components,
92
+ files: options.files,
93
+ flags: resolvedFlags,
94
+ withResponse: true
95
+ };
96
+ if (!isV2) {
97
+ if (options.content !== void 0) payload.content = options.content;
98
+ if (options.embeds !== void 0) payload.embeds = options.embeds;
99
+ }
100
+ const response = await interaction.reply(payload);
101
+ if (response && typeof response === "object" && "resource" in response && response.resource?.message) this.gameMessage = response.resource.message;
102
+ else if (response instanceof Message || response && typeof response === "object" && "id" in response) this.gameMessage = response;
103
+ else if (typeof interaction.fetchReply === "function") this.gameMessage = await interaction.fetchReply().catch(() => null);
104
+ else this.gameMessage = response || null;
105
+ }
106
+ } else {
107
+ const channel = this.channel;
108
+ if (typeof channel.send !== "function") throw new Error("[ContextAdapter] The provided channel is not sendable.");
109
+ const payload = {
110
+ components: options.components,
111
+ files: options.files,
112
+ flags: resolvedFlags,
113
+ allowedMentions: { repliedUser: false }
114
+ };
115
+ if (!isV2) {
116
+ if (options.content !== void 0) payload.content = options.content;
117
+ if (options.embeds !== void 0) payload.embeds = options.embeds;
118
+ }
119
+ const msg = await channel.send(payload);
120
+ this.gameMessage = msg;
121
+ }
122
+ return this.gameMessage;
123
+ }
124
+ async edit(options) {
125
+ if (typeof this.raw?.edit === "function") {
126
+ await this.raw.edit(options);
127
+ return;
128
+ }
129
+ if (!this.gameMessage) {
130
+ await this.sendInitial(options);
131
+ return;
132
+ }
133
+ const resolvedFlags = this.resolveFlags(options);
134
+ const isV2 = !!((resolvedFlags ?? 0) & MessageFlags.IsComponentsV2);
135
+ const payload = {
136
+ components: options.components ?? [],
137
+ files: options.files ?? [],
138
+ flags: resolvedFlags
139
+ };
140
+ if (!isV2) {
141
+ payload.content = options.content ?? null;
142
+ payload.embeds = options.embeds ?? [];
143
+ }
144
+ try {
145
+ await this.gameMessage.edit(payload);
146
+ } catch (err) {
147
+ if (err.code !== 10008 && err.code !== 50027) {}
148
+ }
149
+ }
150
+ static async replyEphemeral(interaction, content) {
151
+ try {
152
+ if (typeof interaction.reply === "function") {
153
+ if (interaction.replied || interaction.deferred) await interaction.followUp({
154
+ content,
155
+ flags: MessageFlags.Ephemeral
156
+ });
157
+ else await interaction.reply({
158
+ content,
159
+ flags: MessageFlags.Ephemeral
160
+ });
161
+ }
162
+ } catch {}
163
+ }
164
+ };
165
+ // src/ui/EcoBaseUI.ts
166
+ /**
167
+ * @m3rcena/eco
168
+ * Base Class for Interactive Discord Economy UI Modules.
169
+ *
170
+ * @copyright 2026 M3rcena Development. All rights reserved.
171
+ * @license Apache-2.0
172
+ */
173
+ var EcoBaseUI = class {
174
+ manager;
175
+ options;
176
+ ctx;
177
+ id;
178
+ defaultColor = 5793266;
179
+ isActive = false;
180
+ timeoutTimer = null;
181
+ defaultTimeout = 12e4;
182
+ constructor(manager, options, prefix = "eco_") {
183
+ this.manager = manager;
184
+ this.options = options;
185
+ this.ctx = ContextAdapter.from(options.context);
186
+ this.id = `${prefix}${Math.random().toString(36).substring(2, 8)}`;
187
+ this.defaultTimeout = options.time || 12e4;
188
+ this.defaultColor = this.resolveColor(options.embedColor || manager?.embedColor);
189
+ }
190
+ resolveColor(color, fallback = 5793266) {
191
+ if (!color) return fallback;
192
+ if (typeof color === "number") return color;
193
+ if (typeof color === "string") {
194
+ if (color.startsWith("#")) {
195
+ const parsed = parseInt(color.replace("#", ""), 16);
196
+ if (!isNaN(parsed)) return parsed;
197
+ }
198
+ }
199
+ try {
200
+ return resolveColor(color);
201
+ } catch {
202
+ return fallback;
203
+ }
204
+ }
205
+ createContainer(color) {
206
+ const container = new ContainerBuilder();
207
+ container.setAccentColor(color !== void 0 ? color : this.defaultColor);
208
+ return container;
209
+ }
210
+ resetTimeout(callback, customMs) {
211
+ this.clearTimer();
212
+ const duration = customMs || this.defaultTimeout;
213
+ this.timeoutTimer = setTimeout(() => {
214
+ if (this.isActive) {
215
+ if (callback) callback();
216
+ else this.destroy();
217
+ }
218
+ }, duration);
219
+ if (typeof this.timeoutTimer.unref === "function") this.timeoutTimer.unref();
220
+ }
221
+ clearTimer() {
222
+ if (this.timeoutTimer) {
223
+ clearTimeout(this.timeoutTimer);
224
+ this.timeoutTimer = null;
225
+ }
226
+ }
227
+ destroy() {
228
+ this.isActive = false;
229
+ this.clearTimer();
230
+ }
231
+ format(template, vars) {
232
+ let result = template;
233
+ for (const [key, value] of Object.entries(vars)) result = result.replaceAll(`{{${key}}}`, String(value));
234
+ return result;
235
+ }
236
+ };
237
+ // src/ui/EcoProfileUI.ts
238
+ /**
239
+ * @m3rcena/eco
240
+ * Interactive Discord Economy Profile & Passport UI.
241
+ *
242
+ * @copyright 2026 M3rcena Development. All rights reserved.
243
+ * @license Apache-2.0
244
+ */
245
+ var EcoProfileUI = class extends EcoBaseUI {
246
+ targetUser;
247
+ currentTab = "OVERVIEW";
248
+ btnPrefix;
249
+ cardBuffer = null;
250
+ constructor(manager, options) {
251
+ super(manager, options, "prof_");
252
+ const target = options.targetUser;
253
+ if (target instanceof GuildMember) this.targetUser = target.user;
254
+ else if (target instanceof User) this.targetUser = target;
255
+ else this.targetUser = this.ctx.user;
256
+ this.btnPrefix = this.id + "_";
257
+ }
258
+ async start() {
259
+ this.isActive = true;
260
+ const account = await this.manager.getAccount(this.targetUser.id, this.ctx.guild?.id);
261
+ if (this.options.renderCard !== false) try {
262
+ const avatarUrl = this.targetUser.displayAvatarURL ? this.targetUser.displayAvatarURL({
263
+ extension: "png",
264
+ size: 128
265
+ }) : void 0;
266
+ this.cardBuffer = await RenderBankCard.renderCard({
267
+ username: this.targetUser.username,
268
+ avatarUrl,
269
+ wallet: account.wallet,
270
+ bank: account.bank,
271
+ netWorth: account.wallet + account.bank,
272
+ currencySymbol: this.manager.currencySymbol
273
+ });
274
+ } catch (err) {
275
+ console.warn("Could not generate canvas bank card:", err);
276
+ }
277
+ const container = this.buildContainer(account);
278
+ const files = this.cardBuffer && this.currentTab === "OVERVIEW" ? [new AttachmentBuilder(this.cardBuffer, { name: "bank-card.png" })] : [];
279
+ await this.ctx.sendInitial({
280
+ components: [container],
281
+ files,
282
+ flags: MessageFlags.IsComponentsV2
283
+ });
284
+ this.setupCollector();
285
+ this.resetTimeout(() => this.destroy());
286
+ }
287
+ setupCollector() {
288
+ if (!this.ctx.channel) return;
289
+ const filter = (i) => i.isButton() && i.customId.startsWith(this.btnPrefix);
290
+ this.ctx.channel.createMessageComponentCollector({
291
+ filter,
292
+ time: this.defaultTimeout
293
+ }).on("collect", async (interaction) => {
294
+ if (interaction.user.id !== this.ctx.userId) {
295
+ ContextAdapter.replyEphemeral(interaction, "⛔ You cannot interact with someone else's profile tabs!");
296
+ return;
297
+ }
298
+ await interaction.deferUpdate().catch(() => {});
299
+ this.resetTimeout(() => this.destroy());
300
+ const action = interaction.customId.replace(this.btnPrefix, "");
301
+ if (action === "tab_overview") this.currentTab = "OVERVIEW";
302
+ else if (action === "tab_inv") this.currentTab = "INVENTORY";
303
+ else if (action === "tab_biz") this.currentTab = "BUSINESSES";
304
+ else if (action === "tab_stocks") this.currentTab = "STOCKS";
305
+ else if (action === "tab_pets") this.currentTab = "PETS";
306
+ const account = await this.manager.getAccount(this.targetUser.id, this.ctx.guild?.id);
307
+ const container = this.buildContainer(account);
308
+ const files = this.cardBuffer && this.currentTab === "OVERVIEW" ? [new AttachmentBuilder(this.cardBuffer, { name: "bank-card.png" })] : [];
309
+ await this.ctx.edit({
310
+ components: [container],
311
+ files,
312
+ flags: MessageFlags.IsComponentsV2
313
+ });
314
+ });
315
+ }
316
+ buildContainer(account) {
317
+ const container = this.createContainer();
318
+ const sym = this.manager.currencySymbol || "🪙";
319
+ let content = "";
320
+ if (this.currentTab === "OVERVIEW") {
321
+ const netWorth = account.wallet + account.bank;
322
+ const job = account.career?.jobTitle || "Unemployed";
323
+ const streak = account.streak?.daily || 0;
324
+ const reqXp = account.level * 250;
325
+ content = `## 💳 Financial Passport: ${this.targetUser.username}\n> **Net Worth:** \`${sym} ${netWorth.toLocaleString()}\` • **Level:** \`${account.level}\` (\`${account.xp}/${reqXp} XP\`)\n> **Cash in Wallet:** \`${sym} ${account.wallet.toLocaleString()}\`\n> **Bank Vault:** \`${sym} ${account.bank.toLocaleString()}\` / \`${sym} ${account.bankCapacity.toLocaleString()}\`\n> **Daily Streak:** \`🔥 ${streak} Days\` • **Career:** \`💼 ${job}\`\n\n*Click the interactive buttons below to inspect inventory, investments, and businesses.*`;
326
+ } else if (this.currentTab === "INVENTORY") {
327
+ const items = account.inventory || [];
328
+ content = `## 🎒 Backpack & Inventory: ${this.targetUser.username}\n`;
329
+ if (items.length === 0) content += `> *Your backpack is completely empty. Visit the shop to purchase tools and upgrades!*`;
330
+ else content += items.map((it) => `> • **${it.name || it.itemId}** (x${it.quantity}) — *${it.category}*`).join("\n");
331
+ } else if (this.currentTab === "BUSINESSES") {
332
+ const biz = account.businesses || [];
333
+ content = `## 🏢 Enterprise Portfolio: ${this.targetUser.username}\n`;
334
+ if (biz.length === 0) content += `> *No companies owned yet. Purchase a business to unlock automated passive income!*`;
335
+ else content += biz.map((b) => `> • **${b.name || b.businessId}** [Level ${b.level}] — 👥 ${b.employees} Staff • Total Rev: \`${sym} ${(b.totalCollected || 0).toLocaleString()}\``).join("\n");
336
+ } else if (this.currentTab === "STOCKS") {
337
+ const stocks = account.stocks || [];
338
+ content = `## 📈 Stock & Crypto Portfolio: ${this.targetUser.username}\n`;
339
+ if (stocks.length === 0) content += `> *No active investments. Browse the stock exchange to trade tech shares and crypto!*`;
340
+ else content += stocks.map((p) => `> • **${p.symbol}**: \`${p.shares} shares\` (Avg Buy: \`$${p.averageBuyPrice.toLocaleString()}\` • Total Invested: \`$${p.totalInvested.toLocaleString()}\`)`).join("\n");
341
+ } else if (this.currentTab === "PETS") {
342
+ const pets = account.pets || [];
343
+ content = `## 🐾 Companion Shelter: ${this.targetUser.username}\n`;
344
+ if (pets.length === 0) content += `> *No companion pets adopted. Adopt a Cybernetic K9 or Lucky Cat to gain foraging loot and defense buffs!*`;
345
+ else content += pets.map((pet) => `> • **${pet.name}** (${pet.species}) [Lvl ${pet.level}] — 🍖 Hunger: \`${pet.hunger}%\` • ❤️ Joy: \`${pet.happiness}%\` ${pet.isExpeditionActive ? "• *[Expedition Active]*" : ""}`).join("\n");
346
+ }
347
+ container.addTextDisplayComponents((t) => t.setContent(content));
348
+ const row = new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId(`${this.btnPrefix}tab_overview`).setLabel("💳 Overview").setStyle(this.currentTab === "OVERVIEW" ? ButtonStyle.Primary : ButtonStyle.Secondary), new ButtonBuilder().setCustomId(`${this.btnPrefix}tab_inv`).setLabel("🎒 Backpack").setStyle(this.currentTab === "INVENTORY" ? ButtonStyle.Primary : ButtonStyle.Secondary), new ButtonBuilder().setCustomId(`${this.btnPrefix}tab_biz`).setLabel("🏢 Businesses").setStyle(this.currentTab === "BUSINESSES" ? ButtonStyle.Primary : ButtonStyle.Secondary), new ButtonBuilder().setCustomId(`${this.btnPrefix}tab_stocks`).setLabel("📈 Stocks").setStyle(this.currentTab === "STOCKS" ? ButtonStyle.Primary : ButtonStyle.Secondary), new ButtonBuilder().setCustomId(`${this.btnPrefix}tab_pets`).setLabel("🐾 Pets").setStyle(this.currentTab === "PETS" ? ButtonStyle.Primary : ButtonStyle.Secondary));
349
+ container.addActionRowComponents(row);
350
+ return container;
351
+ }
352
+ };
353
+ // src/ui/EcoShopUI.ts
354
+ /**
355
+ * @m3rcena/eco
356
+ * Interactive Discord Economy Shop UI with Components V2.
357
+ *
358
+ * @copyright 2026 M3rcena Development. All rights reserved.
359
+ * @license Apache-2.0
360
+ */
361
+ var EcoShopUI = class extends EcoBaseUI {
362
+ selectedCategory = "ALL";
363
+ selectedItemId = null;
364
+ page = 0;
365
+ itemsPerPage = 5;
366
+ btnPrefix;
367
+ constructor(manager, options) {
368
+ super(manager, options, "shop_");
369
+ this.itemsPerPage = options.itemsPerPage || 5;
370
+ this.btnPrefix = this.id + "_";
371
+ }
372
+ async start() {
373
+ this.isActive = true;
374
+ const container = await this.buildContainer();
375
+ await this.ctx.sendInitial({
376
+ components: [container],
377
+ flags: MessageFlags.IsComponentsV2
378
+ });
379
+ this.setupCollector();
380
+ this.resetTimeout(() => this.destroy());
381
+ }
382
+ setupCollector() {
383
+ if (!this.ctx.channel) return;
384
+ const filter = (i) => i.isButton() && i.customId.startsWith(this.btnPrefix) || i.isStringSelectMenu() && i.customId.startsWith(this.btnPrefix);
385
+ this.ctx.channel.createMessageComponentCollector({
386
+ filter,
387
+ time: this.defaultTimeout
388
+ }).on("collect", async (interaction) => {
389
+ if (interaction.user.id !== this.ctx.userId) {
390
+ ContextAdapter.replyEphemeral(interaction, "⛔ You cannot interact with someone else's shop menu!");
391
+ return;
392
+ }
393
+ await interaction.deferUpdate().catch(() => {});
394
+ this.resetTimeout(() => this.destroy());
395
+ if (interaction.isStringSelectMenu()) {
396
+ const value = interaction.values[0];
397
+ if (interaction.customId === `${this.btnPrefix}category_select`) {
398
+ this.selectedCategory = value;
399
+ this.page = 0;
400
+ this.selectedItemId = null;
401
+ } else if (interaction.customId === `${this.btnPrefix}item_select`) this.selectedItemId = value;
402
+ } else if (interaction.isButton()) {
403
+ const action = interaction.customId.replace(this.btnPrefix, "");
404
+ if (action === "prev_page") this.page = Math.max(0, this.page - 1);
405
+ else if (action === "next_page") this.page++;
406
+ else if (action === "buy_1") {
407
+ if (this.selectedItemId) try {
408
+ const res = await this.manager.buyItem(this.ctx.userId, this.selectedItemId, 1, this.ctx.guild?.id);
409
+ if (res.success) ContextAdapter.replyEphemeral(interaction, `✅ Successfully purchased **1x ${res.item?.name || this.selectedItemId}** for ${this.manager.currencySymbol} ${res.totalPrice.toLocaleString()}!`);
410
+ else ContextAdapter.replyEphemeral(interaction, `❌ ${res.message || "Purchase failed."}`);
411
+ } catch (err) {
412
+ ContextAdapter.replyEphemeral(interaction, `❌ Error: ${err.message}`);
413
+ }
414
+ }
415
+ }
416
+ const container = await this.buildContainer();
417
+ await this.ctx.edit({
418
+ components: [container],
419
+ flags: MessageFlags.IsComponentsV2
420
+ });
421
+ });
422
+ }
423
+ async buildContainer() {
424
+ const container = this.createContainer();
425
+ const sym = this.manager.currencySymbol || "🪙";
426
+ const allItems = this.manager.shop ? this.manager.shop.getItems() : [];
427
+ const categories = Array.from(new Set(allItems.map((i) => i.category.toUpperCase())));
428
+ const filtered = allItems.filter((item) => this.selectedCategory === "ALL" ? true : item.category.toUpperCase() === this.selectedCategory);
429
+ const totalPages = Math.ceil(filtered.length / this.itemsPerPage) || 1;
430
+ if (this.page >= totalPages) this.page = totalPages - 1;
431
+ const startIdx = this.page * this.itemsPerPage;
432
+ const pageItems = filtered.slice(startIdx, startIdx + this.itemsPerPage);
433
+ if (!this.selectedItemId && pageItems.length > 0) this.selectedItemId = pageItems[0].id;
434
+ else if (this.selectedItemId && !pageItems.find((i) => i.id === this.selectedItemId) && pageItems.length > 0) this.selectedItemId = pageItems[0].id;
435
+ const selectedItem = allItems.find((i) => i.id === this.selectedItemId);
436
+ let content = `## 🛒 Nexus Marketplace\n> **Category:** \`${this.selectedCategory}\` • **Page:** \`${this.page + 1}/${totalPages}\`\n\n`;
437
+ if (pageItems.length === 0) content += `> *No items available in this category.*\n`;
438
+ else for (const item of pageItems) {
439
+ const isSel = item.id === this.selectedItemId ? "👉 " : "• ";
440
+ content += `${isSel}**${item.name}** [${sym} ${item.price.toLocaleString()}] — *${item.description}*\n`;
441
+ }
442
+ if (selectedItem) content += `\n### 📦 Selected Item: ${selectedItem.name}\n> **Price:** \`${sym} ${selectedItem.price.toLocaleString()}\` • **Type:** \`${selectedItem.category}\`\n> **Description:** ${selectedItem.description}\n`;
443
+ container.addTextDisplayComponents((t) => t.setContent(content));
444
+ const catOptions = [new StringSelectMenuOptionBuilder().setLabel("All Categories").setValue("ALL").setDescription("Browse entire catalog").setDefault(this.selectedCategory === "ALL"), ...categories.map((cat) => new StringSelectMenuOptionBuilder().setLabel(cat.charAt(0) + cat.slice(1).toLowerCase()).setValue(cat).setDescription(`Filter by ${cat.toLowerCase()}`).setDefault(this.selectedCategory === cat))];
445
+ const catRow = new ActionRowBuilder().addComponents(new StringSelectMenuBuilder().setCustomId(`${this.btnPrefix}category_select`).setPlaceholder("Filter by Category...").addOptions(catOptions));
446
+ container.addActionRowComponents(catRow);
447
+ if (pageItems.length > 0) {
448
+ const itemOptions = pageItems.map((it) => new StringSelectMenuOptionBuilder().setLabel(it.name).setValue(it.id).setDescription(`${sym} ${it.price.toLocaleString()} — ${it.category}`).setDefault(it.id === this.selectedItemId));
449
+ const itemRow = new ActionRowBuilder().addComponents(new StringSelectMenuBuilder().setCustomId(`${this.btnPrefix}item_select`).setPlaceholder("Select an item to inspect or purchase...").addOptions(itemOptions));
450
+ container.addActionRowComponents(itemRow);
451
+ }
452
+ const btnRow = new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId(`${this.btnPrefix}prev_page`).setLabel("◀ Previous").setStyle(ButtonStyle.Secondary).setDisabled(this.page === 0), new ButtonBuilder().setCustomId(`${this.btnPrefix}buy_1`).setLabel(`Buy Selected`).setStyle(ButtonStyle.Success).setDisabled(!selectedItem), new ButtonBuilder().setCustomId(`${this.btnPrefix}next_page`).setLabel("Next ▶").setStyle(ButtonStyle.Secondary).setDisabled(this.page >= totalPages - 1));
453
+ container.addActionRowComponents(btnRow);
454
+ return container;
455
+ }
456
+ };
457
+ // src/ui/EcoStockMarketUI.ts
458
+ /**
459
+ * @m3rcena/eco
460
+ * Interactive Discord Stock & Crypto Exchange Terminal UI.
461
+ *
462
+ * @copyright 2026 M3rcena Development. All rights reserved.
463
+ * @license Apache-2.0
464
+ */
465
+ var EcoStockMarketUI = class extends EcoBaseUI {
466
+ selectedSymbol = "$AETH";
467
+ chartBuffer = null;
468
+ btnPrefix;
469
+ constructor(manager, options) {
470
+ super(manager, options, "stock_");
471
+ this.btnPrefix = this.id + "_";
472
+ }
473
+ async start() {
474
+ this.isActive = true;
475
+ const stocks = this.getStocks();
476
+ if (stocks.length > 0 && !stocks.find((s) => s.symbol === this.selectedSymbol)) this.selectedSymbol = stocks[0].symbol;
477
+ await this.updateChart();
478
+ const container = await this.buildContainer();
479
+ const files = this.chartBuffer ? [new AttachmentBuilder(this.chartBuffer, { name: "market-chart.png" })] : [];
480
+ await this.ctx.sendInitial({
481
+ components: [container],
482
+ files,
483
+ flags: MessageFlags.IsComponentsV2
484
+ });
485
+ this.setupCollector();
486
+ this.resetTimeout(() => this.destroy());
487
+ }
488
+ getStocks() {
489
+ if (this.manager.stocks && typeof this.manager.stocks.getStocks === "function") return this.manager.stocks.getStocks();
490
+ return [];
491
+ }
492
+ async updateChart() {
493
+ if (this.options.renderChart === false) return;
494
+ const stock = this.getStocks().find((s) => s.symbol.toUpperCase() === this.selectedSymbol.toUpperCase());
495
+ if (!stock) return;
496
+ try {
497
+ this.chartBuffer = await RenderMarketChart.renderChart({
498
+ symbol: stock.symbol,
499
+ name: stock.name,
500
+ currentPrice: stock.currentPrice,
501
+ change24h: stock.change24h,
502
+ history: stock.history
503
+ });
504
+ } catch (err) {
505
+ console.warn("Could not render market chart:", err);
506
+ }
507
+ }
508
+ setupCollector() {
509
+ if (!this.ctx.channel) return;
510
+ const filter = (i) => i.isButton() && i.customId.startsWith(this.btnPrefix) || i.isStringSelectMenu() && i.customId.startsWith(this.btnPrefix);
511
+ this.ctx.channel.createMessageComponentCollector({
512
+ filter,
513
+ time: this.defaultTimeout
514
+ }).on("collect", async (interaction) => {
515
+ if (interaction.user.id !== this.ctx.userId) {
516
+ ContextAdapter.replyEphemeral(interaction, "⛔ You cannot trade on someone else's market terminal!");
517
+ return;
518
+ }
519
+ await interaction.deferUpdate().catch(() => {});
520
+ this.resetTimeout(() => this.destroy());
521
+ if (interaction.isStringSelectMenu()) {
522
+ if (interaction.customId === `${this.btnPrefix}select_ticker`) {
523
+ this.selectedSymbol = interaction.values[0];
524
+ await this.updateChart();
525
+ }
526
+ } else if (interaction.isButton()) {
527
+ const action = interaction.customId.replace(this.btnPrefix, "");
528
+ if (action === "buy_1") try {
529
+ const res = await this.manager.buyStock(this.ctx.userId, this.selectedSymbol, 1, this.ctx.guild?.id);
530
+ ContextAdapter.replyEphemeral(interaction, `📈 Bought 1 share of **${this.selectedSymbol}** at $${res.pricePerShare.toLocaleString()} (Total: $${res.totalAmount.toLocaleString()})`);
531
+ } catch (err) {
532
+ ContextAdapter.replyEphemeral(interaction, `❌ ${err.message}`);
533
+ }
534
+ else if (action === "buy_10") try {
535
+ const res = await this.manager.buyStock(this.ctx.userId, this.selectedSymbol, 10, this.ctx.guild?.id);
536
+ ContextAdapter.replyEphemeral(interaction, `📈 Bought 10 shares of **${this.selectedSymbol}** at $${res.pricePerShare.toLocaleString()} (Total: $${res.totalAmount.toLocaleString()})`);
537
+ } catch (err) {
538
+ ContextAdapter.replyEphemeral(interaction, `❌ ${err.message}`);
539
+ }
540
+ else if (action === "sell_all") try {
541
+ const pos = (await this.manager.getStockPortfolio(this.ctx.userId, this.ctx.guild?.id)).positions.find((p) => p.symbol.toUpperCase() === this.selectedSymbol.toUpperCase());
542
+ if (!pos || pos.shares <= 0) ContextAdapter.replyEphemeral(interaction, `❌ You don't own any shares of **${this.selectedSymbol}**.`);
543
+ else {
544
+ const res = await this.manager.sellStock(this.ctx.userId, this.selectedSymbol, pos.shares, this.ctx.guild?.id);
545
+ const pnlPrefix = (res.profitOrLoss ?? 0) >= 0 ? "+$" : "-$";
546
+ ContextAdapter.replyEphemeral(interaction, `📉 Sold ${res.shares} shares of **${this.selectedSymbol}** for $${res.totalAmount.toLocaleString()}! (PnL: ${pnlPrefix}${Math.abs(res.profitOrLoss ?? 0).toLocaleString()})`);
547
+ }
548
+ } catch (err) {
549
+ ContextAdapter.replyEphemeral(interaction, `❌ ${err.message}`);
550
+ }
551
+ else if (action === "refresh") {
552
+ if (this.manager.stocks && typeof this.manager.stocks.tickMarket === "function") this.manager.stocks.tickMarket();
553
+ await this.updateChart();
554
+ }
555
+ }
556
+ const container = await this.buildContainer();
557
+ const files = this.chartBuffer ? [new AttachmentBuilder(this.chartBuffer, { name: "market-chart.png" })] : [];
558
+ await this.ctx.edit({
559
+ components: [container],
560
+ files,
561
+ flags: MessageFlags.IsComponentsV2
562
+ });
563
+ });
564
+ }
565
+ async buildContainer() {
566
+ const container = this.createContainer();
567
+ const stocks = this.getStocks();
568
+ const stock = stocks.find((s) => s.symbol.toUpperCase() === this.selectedSymbol.toUpperCase()) || stocks[0];
569
+ const trend = this.manager.stocks?.getMarketTrend?.() || "STABLE";
570
+ let portfolio = {
571
+ positions: [],
572
+ totalValue: 0,
573
+ totalProfitLoss: 0
574
+ };
575
+ try {
576
+ portfolio = await this.manager.getStockPortfolio(this.ctx.userId, this.ctx.guild?.id);
577
+ } catch {}
578
+ const userPos = portfolio.positions?.find((p) => p.symbol.toUpperCase() === (stock?.symbol || "").toUpperCase());
579
+ const sharesOwned = userPos ? userPos.shares : 0;
580
+ const avgBuy = userPos ? userPos.averageBuyPrice : 0;
581
+ const currentVal = stock ? sharesOwned * stock.currentPrice : 0;
582
+ const pnl = userPos ? currentVal - userPos.totalInvested : 0;
583
+ const pnlSign = pnl >= 0 ? "+" : "";
584
+ const trendEmoji = trend === "BULL" ? "🟢 🐂 BULL MARKET" : trend === "BEAR" ? "🔴 🐻 BEAR MARKET" : "🟡 ⚖️ STABLE MARKET";
585
+ const changeSign = stock && stock.change24h >= 0 ? "+" : "";
586
+ let content = `## 📊 Global Financial & Crypto Exchange\n> **Market Mood:** \`${trendEmoji}\` • **Total Portfolio Value:** \`$${(portfolio.totalValue || 0).toLocaleString()}\`\n\n`;
587
+ if (stock) content += `### 🏷️ Active Asset: ${stock.name} (${stock.symbol})\n> **Current Price:** \`$${stock.currentPrice.toLocaleString()}\` (${changeSign}${stock.change24h}% 24h)\n> **24h Range:** \`$${stock.low24h.toLocaleString()}\` — \`$${stock.high24h.toLocaleString()}\` • **Volatility:** \`${(stock.volatility * 100).toFixed(0)}%\`\n> **Sector:** \`${stock.category}\` • **Info:** *${stock.description}*\n\n> **Your Holdings:** \`${sharesOwned} shares\` (Valued at \`$${currentVal.toLocaleString()}\`)\n> **Unrealized PnL:** \`${pnlSign}$${pnl.toLocaleString()}\` (Avg Buy: \`$${avgBuy.toLocaleString()}\`)\n`;
588
+ container.addTextDisplayComponents((t) => t.setContent(content));
589
+ if (stocks.length > 0) {
590
+ const options = stocks.map((s) => new StringSelectMenuOptionBuilder().setLabel(`${s.symbol} — ${s.name}`).setValue(s.symbol).setDescription(`$${s.currentPrice.toLocaleString()} (${s.change24h >= 0 ? "+" : ""}${s.change24h}%)`).setDefault(s.symbol.toUpperCase() === this.selectedSymbol.toUpperCase()));
591
+ const selectRow = new ActionRowBuilder().addComponents(new StringSelectMenuBuilder().setCustomId(`${this.btnPrefix}select_ticker`).setPlaceholder("Select an asset / cryptocurrency...").addOptions(options));
592
+ container.addActionRowComponents(selectRow);
593
+ }
594
+ const actionRow = new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId(`${this.btnPrefix}buy_1`).setLabel("Buy 1x").setStyle(ButtonStyle.Success), new ButtonBuilder().setCustomId(`${this.btnPrefix}buy_10`).setLabel("Buy 10x").setStyle(ButtonStyle.Success), new ButtonBuilder().setCustomId(`${this.btnPrefix}sell_all`).setLabel("Sell All").setStyle(ButtonStyle.Danger).setDisabled(sharesOwned <= 0), new ButtonBuilder().setCustomId(`${this.btnPrefix}refresh`).setLabel("🔄 Live Refresh").setStyle(ButtonStyle.Secondary));
595
+ container.addActionRowComponents(actionRow);
596
+ return container;
597
+ }
598
+ };
599
+ // src/ui/EcoHeistUI.ts
600
+ /**
601
+ * @m3rcena/eco
602
+ * Interactive Discord Multiplayer Crew Bank Heist UI.
603
+ *
604
+ * @copyright 2026 M3rcena Development. All rights reserved.
605
+ * @license Apache-2.0
606
+ */
607
+ var EcoHeistUI = class extends EcoBaseUI {
608
+ targetId = "city_trust_bank";
609
+ session = null;
610
+ btnPrefix;
611
+ statusMessage = "Waiting for crew members to select their roles...";
612
+ isExecuting = false;
613
+ outcome = null;
614
+ constructor(manager, options) {
615
+ super(manager, options, "heist_");
616
+ this.targetId = options.initialTargetId || "city_trust_bank";
617
+ this.btnPrefix = this.id + "_";
618
+ }
619
+ async start() {
620
+ this.isActive = true;
621
+ try {
622
+ this.session = await this.manager.createHeist(this.ctx.userId, this.targetId, this.ctx.guild?.id);
623
+ } catch (err) {
624
+ this.statusMessage = `Error initializing heist: ${err.message}`;
625
+ }
626
+ const container = this.buildContainer();
627
+ await this.ctx.sendInitial({
628
+ components: [container],
629
+ flags: MessageFlags.IsComponentsV2
630
+ });
631
+ this.setupCollector();
632
+ this.resetTimeout(() => this.destroy(), 18e4);
633
+ }
634
+ setupCollector() {
635
+ if (!this.ctx.channel) return;
636
+ const filter = (i) => i.isButton() && i.customId.startsWith(this.btnPrefix) || i.isStringSelectMenu() && i.customId.startsWith(this.btnPrefix);
637
+ this.ctx.channel.createMessageComponentCollector({
638
+ filter,
639
+ time: 18e4
640
+ }).on("collect", async (interaction) => {
641
+ if (this.isExecuting || this.outcome) {
642
+ ContextAdapter.replyEphemeral(interaction, "⚠️ The heist has already been executed.");
643
+ return;
644
+ }
645
+ await interaction.deferUpdate().catch(() => {});
646
+ this.resetTimeout(() => this.destroy(), 18e4);
647
+ if (interaction.isStringSelectMenu()) {
648
+ if (interaction.customId === `${this.btnPrefix}select_target`) {
649
+ if (interaction.user.id !== this.ctx.userId) {
650
+ ContextAdapter.replyEphemeral(interaction, "⛔ Only the crew leader can change the vault target!");
651
+ return;
652
+ }
653
+ this.targetId = interaction.values[0];
654
+ try {
655
+ this.session = await this.manager.createHeist(this.ctx.userId, this.targetId, this.ctx.guild?.id);
656
+ } catch {}
657
+ }
658
+ } else if (interaction.isButton()) {
659
+ const action = interaction.customId.replace(this.btnPrefix, "");
660
+ if (action.startsWith("join_")) {
661
+ const role = action.replace("join_", "").toUpperCase();
662
+ try {
663
+ if (this.session) {
664
+ const username = interaction.user.username;
665
+ const res = await this.manager.joinHeist(this.session.id, interaction.user.id, role, username);
666
+ this.session = res.session;
667
+ this.statusMessage = `🤝 **${username}** joined as the **${role}**!`;
668
+ }
669
+ } catch (err) {
670
+ ContextAdapter.replyEphemeral(interaction, `❌ ${err.message}`);
671
+ }
672
+ } else if (action === "execute") {
673
+ if (interaction.user.id !== this.ctx.userId) {
674
+ ContextAdapter.replyEphemeral(interaction, "⛔ Only the crew leader can trigger the breach!");
675
+ return;
676
+ }
677
+ if (!this.session) return;
678
+ const target = this.getTarget();
679
+ if (this.session.crew.length < target.minCrew) {
680
+ ContextAdapter.replyEphemeral(interaction, `❌ Need at least ${target.minCrew} crew members to attempt this vault! Currently: ${this.session.crew.length}`);
681
+ return;
682
+ }
683
+ this.isExecuting = true;
684
+ this.statusMessage = "🚨 **BREACH IN PROGRESS!** Cutting power grid & drilling safety vault...";
685
+ const interimContainer = this.buildContainer();
686
+ await this.ctx.edit({
687
+ components: [interimContainer],
688
+ flags: MessageFlags.IsComponentsV2
689
+ });
690
+ try {
691
+ this.outcome = await this.manager.executeHeist(this.session.id);
692
+ if (this.outcome?.success) this.statusMessage = `🏆 **VAULT CLEANED OUT!** Stole **$${this.outcome.totalLoot.toLocaleString()}**!`;
693
+ else this.statusMessage = `🚔 **BUSTED BY SWAT!** Heist failed. Bail fee paid: $${(this.outcome?.finePerPlayer || 0).toLocaleString()}`;
694
+ } catch (err) {
695
+ this.statusMessage = `❌ Execution failed: ${err.message}`;
696
+ } finally {
697
+ this.isExecuting = false;
698
+ }
699
+ }
700
+ }
701
+ const container = this.buildContainer();
702
+ await this.ctx.edit({
703
+ components: [container],
704
+ flags: MessageFlags.IsComponentsV2
705
+ });
706
+ });
707
+ }
708
+ getTarget() {
709
+ const targets = this.manager.heist?.getTargets?.() || DEFAULT_HEIST_TARGETS;
710
+ return targets.find((t) => t.id === this.targetId) || targets[0];
711
+ }
712
+ buildContainer() {
713
+ const container = this.createContainer();
714
+ const target = this.getTarget();
715
+ const crew = this.session?.crew || [];
716
+ const sym = this.manager.currencySymbol || "🪙";
717
+ let content = `## 🏦 Syndicate Crew Bank Heist Operations\n> **Leader:** <@${this.ctx.userId}> • **Target:** \`${target.name}\` [${target.difficulty}]\n> **Potential Haul:** \`${sym} ${target.baseBountyMin.toLocaleString()}\` — \`${sym} ${target.baseBountyMax.toLocaleString()}\`\n> **Failure Bail Risk:** \`${sym} ${target.failureBail.toLocaleString()}\` • **Base Success Odds:** \`${(target.successRate * 100).toFixed(0)}%\`\n\n> **Status:** ${this.statusMessage}\n\n`;
718
+ content += `### 👥 Active Crew Roster (${crew.length}/${target.maxCrew})\n`;
719
+ if (crew.length === 0) content += `> *No crew members assembled yet. Claim a role below to gear up!*\n`;
720
+ else for (const member of crew) {
721
+ const icon = member.role === "HACKER" ? "💻" : member.role === "DRIVER" ? "🚗" : member.role === "DEMOLITIONIST" ? "💣" : "💪";
722
+ content += `> ${icon} **${member.role}**: **${member.username}** (<@${member.userId}>)\n`;
723
+ }
724
+ if (this.outcome) {
725
+ content += `\n### 📋 After-Action Report\n`;
726
+ if (this.outcome.success) content += `> 💰 **Total Loot Taken:** \`${sym} ${this.outcome.totalLoot.toLocaleString()}\`\n> 💵 **Cut Per Operative:** \`${sym} ${this.outcome.sharePerPlayer.toLocaleString()}\`\n`;
727
+ else content += `> 🚔 **Interception:** Police interceptors cornered the team.\n> 💸 **Bail Deducted:** \`${sym} ${(this.outcome.finePerPlayer || 0).toLocaleString()}\` each\n`;
728
+ }
729
+ container.addTextDisplayComponents((t) => t.setContent(content));
730
+ if (!this.isExecuting && !this.outcome) {
731
+ const targetOptions = (this.manager.heist?.getTargets?.() || DEFAULT_HEIST_TARGETS).map((t) => new StringSelectMenuOptionBuilder().setLabel(`${t.name} [${t.difficulty}]`).setValue(t.id).setDescription(`Crew: ${t.minCrew}-${t.maxCrew} | Payout: ${sym} ${t.baseBountyMin.toLocaleString()}+`).setDefault(t.id === this.targetId));
732
+ const targetRow = new ActionRowBuilder().addComponents(new StringSelectMenuBuilder().setCustomId(`${this.btnPrefix}select_target`).setPlaceholder("Select a vault target...").addOptions(targetOptions));
733
+ container.addActionRowComponents(targetRow);
734
+ const roleRow = new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId(`${this.btnPrefix}join_hacker`).setLabel("💻 Hacker").setStyle(ButtonStyle.Secondary), new ButtonBuilder().setCustomId(`${this.btnPrefix}join_driver`).setLabel("🚗 Driver").setStyle(ButtonStyle.Secondary), new ButtonBuilder().setCustomId(`${this.btnPrefix}join_demolitionist`).setLabel("💣 Demo").setStyle(ButtonStyle.Secondary), new ButtonBuilder().setCustomId(`${this.btnPrefix}join_muscle`).setLabel("💪 Muscle").setStyle(ButtonStyle.Secondary));
735
+ container.addActionRowComponents(roleRow);
736
+ const canLaunch = crew.length >= target.minCrew && crew.length <= target.maxCrew;
737
+ const launchRow = new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId(`${this.btnPrefix}execute`).setLabel(canLaunch ? "🚀 Launch Heist Breach" : `Need ${target.minCrew}+ Crew to Launch`).setStyle(canLaunch ? ButtonStyle.Success : ButtonStyle.Secondary).setDisabled(!canLaunch));
738
+ container.addActionRowComponents(launchRow);
739
+ }
740
+ return container;
741
+ }
742
+ };
743
+ // src/ui/EcoBlackjackUI.ts
744
+ /**
745
+ * @m3rcena/eco
746
+ * Interactive Discord Blackjack Casino UI with Components V2.
747
+ *
748
+ * @copyright 2026 M3rcena Development. All rights reserved.
749
+ * @license Apache-2.0
750
+ */
751
+ var EcoBlackjackUI = class extends EcoBaseUI {
752
+ state = null;
753
+ btnPrefix;
754
+ initialBet;
755
+ constructor(manager, options) {
756
+ super(manager, options, "bj_");
757
+ this.initialBet = Math.max(10, Math.floor(options.betAmount || 50));
758
+ this.btnPrefix = this.id + "_";
759
+ }
760
+ async start() {
761
+ this.isActive = true;
762
+ try {
763
+ this.state = await this.manager.startBlackjack(this.ctx.userId, this.initialBet, this.ctx.guild?.id);
764
+ } catch (err) {
765
+ await this.ctx.sendInitial({ content: `❌ Could not start Blackjack game: ${err.message}` });
766
+ this.destroy();
767
+ return;
768
+ }
769
+ const container = this.buildContainer();
770
+ await this.ctx.sendInitial({
771
+ components: [container],
772
+ flags: MessageFlags.IsComponentsV2
773
+ });
774
+ if (this.state && this.state.finished) {
775
+ this.destroy();
776
+ return;
777
+ }
778
+ this.setupCollector();
779
+ this.resetTimeout(() => this.destroy(), 6e4);
780
+ }
781
+ setupCollector() {
782
+ if (!this.ctx.channel) return;
783
+ const filter = (i) => i.isButton() && i.customId.startsWith(this.btnPrefix);
784
+ const collector = this.ctx.channel.createMessageComponentCollector({
785
+ filter,
786
+ time: 6e4
787
+ });
788
+ collector.on("collect", async (interaction) => {
789
+ if (interaction.user.id !== this.ctx.userId) {
790
+ ContextAdapter.replyEphemeral(interaction, "⛔ You cannot play on someone else's blackjack table!");
791
+ return;
792
+ }
793
+ if (!this.state || this.state.finished) {
794
+ ContextAdapter.replyEphemeral(interaction, "⚠️ This hand has already concluded.");
795
+ return;
796
+ }
797
+ await interaction.deferUpdate().catch(() => {});
798
+ this.resetTimeout(() => this.destroy(), 6e4);
799
+ const action = interaction.customId.replace(this.btnPrefix, "");
800
+ try {
801
+ if (action === "hit") this.state = await this.manager.blackjackHit(this.state.gameId);
802
+ else if (action === "stand") this.state = await this.manager.blackjackStand(this.state.gameId);
803
+ else if (action === "double") this.state = await this.manager.blackjackDouble(this.state.gameId);
804
+ } catch (err) {
805
+ ContextAdapter.replyEphemeral(interaction, `❌ ${err.message}`);
806
+ }
807
+ const container = this.buildContainer();
808
+ await this.ctx.edit({
809
+ components: [container],
810
+ flags: MessageFlags.IsComponentsV2
811
+ });
812
+ if (this.state?.finished) {
813
+ collector.stop();
814
+ this.destroy();
815
+ }
816
+ });
817
+ }
818
+ formatCards(cards, hideSecond = false) {
819
+ if (hideSecond && cards.length > 1) return `[\`${cards[0].rank}${cards[0].suit}\`] [\`🂠 ?\`]`;
820
+ return cards.map((c) => `[\`${c.rank}${c.suit}\`]`).join(" ");
821
+ }
822
+ buildContainer() {
823
+ const container = this.createContainer();
824
+ const sym = this.manager.currencySymbol || "🪙";
825
+ if (!this.state) {
826
+ container.addTextDisplayComponents((t) => t.setContent("## 🎰 Blackjack Table\n> *No game in progress.*"));
827
+ return container;
828
+ }
829
+ const isPlaying = !this.state.finished;
830
+ const pCalc = this.manager.casino.calculateHand(this.state.playerHand);
831
+ const dCalc = this.manager.casino.calculateHand(this.state.dealerHand);
832
+ let statusText = "Game in progress... Hit or Stand?";
833
+ if (this.state.status === "blackjack") statusText = `🏆 **NATURAL BLACKJACK!** Payout: **${sym} ${this.state.payout.toLocaleString()}** (3:2)`;
834
+ else if (this.state.status === "player_win") statusText = `🎉 **YOU WIN!** Dealer lost. Payout: **${sym} ${this.state.payout.toLocaleString()}**`;
835
+ else if (this.state.status === "dealer_bust") statusText = `💥 **DEALER BUSTED!** Payout: **${sym} ${this.state.payout.toLocaleString()}**`;
836
+ else if (this.state.status === "player_bust") statusText = `💥 **BUSTED!** Total exceeded 21. Lost: **${sym} ${this.state.betAmount.toLocaleString()}**`;
837
+ else if (this.state.status === "dealer_win") statusText = `💀 **DEALER WINS!** Dealer had the superior hand. Lost: **${sym} ${this.state.betAmount.toLocaleString()}**`;
838
+ else if (this.state.status === "push") statusText = `🤝 **PUSH (TIE)!** Bet of **${sym} ${this.state.betAmount.toLocaleString()}** refunded.`;
839
+ const dealerDisplay = isPlaying ? `${this.formatCards(this.state.dealerHand, true)} *(Showing ${this.state.dealerHand[0].value})*` : `${this.formatCards(this.state.dealerHand, false)} *(Total: **${dCalc.total}**)*`;
840
+ const playerDisplay = `${this.formatCards(this.state.playerHand, false)} *(Total: **${pCalc.total}**${pCalc.isSoft ? " soft" : ""})*`;
841
+ let content = `## 🎰 High Stakes VIP Blackjack Table\n> **Player:** <@${this.ctx.userId}> • **Total Bet:** \`${sym} ${this.state.betAmount.toLocaleString()}\`\n\n### 🤵 Dealer's Hand:\n> ${dealerDisplay}\n\n### 🃏 Your Hand:\n> ${playerDisplay}\n\n> **Table Status:** ${statusText}\n`;
842
+ container.addTextDisplayComponents((t) => t.setContent(content));
843
+ const canDouble = isPlaying && this.state.playerHand.length === 2;
844
+ const actionRow = new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId(`${this.btnPrefix}hit`).setLabel("🃏 Hit").setStyle(ButtonStyle.Success).setDisabled(!isPlaying), new ButtonBuilder().setCustomId(`${this.btnPrefix}stand`).setLabel("🛑 Stand").setStyle(ButtonStyle.Danger).setDisabled(!isPlaying), new ButtonBuilder().setCustomId(`${this.btnPrefix}double`).setLabel("⚡ Double Down").setStyle(ButtonStyle.Primary).setDisabled(!canDouble));
845
+ container.addActionRowComponents(actionRow);
846
+ return container;
847
+ }
848
+ };
849
+ // src/ui/EcoCrateUI.ts
850
+ /**
851
+ * @m3rcena/eco
852
+ * Interactive Discord Mystery Loot Crate Unboxing UI with Components V2.
853
+ *
854
+ * @copyright 2026 M3rcena Development. All rights reserved.
855
+ * @license Apache-2.0
856
+ */
857
+ var EcoCrateUI = class extends EcoBaseUI {
858
+ selectedTier = "rare";
859
+ btnPrefix;
860
+ isUnboxing = false;
861
+ lastReward = null;
862
+ constructor(manager, options) {
863
+ super(manager, options, "crate_");
864
+ this.selectedTier = options.initialTier || "rare";
865
+ this.btnPrefix = this.id + "_";
866
+ }
867
+ async start() {
868
+ this.isActive = true;
869
+ const container = this.buildContainer();
870
+ await this.ctx.sendInitial({
871
+ components: [container],
872
+ flags: MessageFlags.IsComponentsV2
873
+ });
874
+ this.setupCollector();
875
+ this.resetTimeout(() => this.destroy());
876
+ }
877
+ setupCollector() {
878
+ if (!this.ctx.channel) return;
879
+ const filter = (i) => i.isButton() && i.customId.startsWith(this.btnPrefix) || i.isStringSelectMenu() && i.customId.startsWith(this.btnPrefix);
880
+ this.ctx.channel.createMessageComponentCollector({
881
+ filter,
882
+ time: this.defaultTimeout
883
+ }).on("collect", async (interaction) => {
884
+ if (interaction.user.id !== this.ctx.userId) {
885
+ ContextAdapter.replyEphemeral(interaction, "⛔ You cannot open someone else's mystery crates!");
886
+ return;
887
+ }
888
+ await interaction.deferUpdate().catch(() => {});
889
+ this.resetTimeout(() => this.destroy());
890
+ if (interaction.isStringSelectMenu()) {
891
+ if (interaction.customId === `${this.btnPrefix}select_tier`) {
892
+ this.selectedTier = interaction.values[0];
893
+ this.lastReward = null;
894
+ }
895
+ } else if (interaction.isButton()) {
896
+ if (interaction.customId.replace(this.btnPrefix, "") === "open") {
897
+ this.isUnboxing = true;
898
+ this.lastReward = null;
899
+ const suspenseContainer = this.buildContainer();
900
+ await this.ctx.edit({
901
+ components: [suspenseContainer],
902
+ flags: MessageFlags.IsComponentsV2
903
+ });
904
+ try {
905
+ this.lastReward = await this.manager.openCrate(this.ctx.userId, this.selectedTier, this.ctx.guild?.id);
906
+ } catch (err) {
907
+ ContextAdapter.replyEphemeral(interaction, `❌ ${err.message}`);
908
+ } finally {
909
+ this.isUnboxing = false;
910
+ }
911
+ }
912
+ }
913
+ const container = this.buildContainer();
914
+ await this.ctx.edit({
915
+ components: [container],
916
+ flags: MessageFlags.IsComponentsV2
917
+ });
918
+ });
919
+ }
920
+ buildContainer() {
921
+ const container = this.createContainer();
922
+ const sym = this.manager.currencySymbol || "🪙";
923
+ const cost = CRATE_PRICES[this.selectedTier] || 1e4;
924
+ const tierIcons = {
925
+ common: "📦 Common Supply Drop",
926
+ rare: "🔷 Rare Tech Capsule",
927
+ epic: "💜 Epic Cyber Crate",
928
+ legendary: "👑 Legendary Vault Box",
929
+ mythic: "🌌 Mythic Celestial Relic"
930
+ };
931
+ let content = `## 🎁 Quantum Mystery Loot Crates\n> **Selected Tier:** \`${tierIcons[this.selectedTier]}\`\n> **Price to Unlock:** \`${sym} ${cost.toLocaleString()}\`\n\n`;
932
+ if (this.isUnboxing) content += "> ⏳ **UNBOXING IN PROGRESS...**\n> *Decentralized cryptographic locks cracking... Rolling loot tables...*\n\n";
933
+ else if (this.lastReward) {
934
+ content += `### 🎊 Loot Drop Received!\n> 💰 **Cash Rewarded:** \`${sym} ${this.lastReward.coins.toLocaleString()}\`\n> ⭐ **Experience Points:** \`+${this.lastReward.xp} XP\`\n`;
935
+ if (this.lastReward.item) content += `> 🎒 **Special Item Received:** **${this.lastReward.item.name}** (*${this.lastReward.item.category}*)\n`;
936
+ content += `\n`;
937
+ } else content += `> *Select a crate tier from the menu below and press Open to reveal randomized riches, XP, and collectible items!* \n\n`;
938
+ container.addTextDisplayComponents((t) => t.setContent(content));
939
+ if (!this.isUnboxing) {
940
+ const options = [
941
+ "common",
942
+ "rare",
943
+ "epic",
944
+ "legendary",
945
+ "mythic"
946
+ ].map((t) => new StringSelectMenuOptionBuilder().setLabel(tierIcons[t]).setValue(t).setDescription(`${sym} ${CRATE_PRICES[t].toLocaleString()}`).setDefault(t === this.selectedTier));
947
+ const menuRow = new ActionRowBuilder().addComponents(new StringSelectMenuBuilder().setCustomId(`${this.btnPrefix}select_tier`).setPlaceholder("Select Crate Tier...").addOptions(options));
948
+ container.addActionRowComponents(menuRow);
949
+ const openRow = new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId(`${this.btnPrefix}open`).setLabel(`🎁 Open ${this.selectedTier.toUpperCase()} (${sym} ${cost.toLocaleString()})`).setStyle(ButtonStyle.Success));
950
+ container.addActionRowComponents(openRow);
951
+ }
952
+ return container;
953
+ }
954
+ };
955
+ // src/ui/EcoLeaderboardUI.ts
956
+ /**
957
+ * @m3rcena/eco
958
+ * Interactive Discord Leaderboard UI with Components V2.
959
+ *
960
+ * @copyright 2026 M3rcena Development. All rights reserved.
961
+ * @license Apache-2.0
962
+ */
963
+ var EcoLeaderboardUI = class extends EcoBaseUI {
964
+ selectedMetric = "net_worth";
965
+ page = 0;
966
+ pageSize = 10;
967
+ btnPrefix;
968
+ constructor(manager, options) {
969
+ super(manager, options, "lb_");
970
+ this.selectedMetric = options.initialMetric || "net_worth";
971
+ this.btnPrefix = this.id + "_";
972
+ }
973
+ async start() {
974
+ this.isActive = true;
975
+ const container = await this.buildContainer();
976
+ await this.ctx.sendInitial({
977
+ components: [container],
978
+ flags: MessageFlags.IsComponentsV2
979
+ });
980
+ this.setupCollector();
981
+ this.resetTimeout(() => this.destroy());
982
+ }
983
+ setupCollector() {
984
+ if (!this.ctx.channel) return;
985
+ const filter = (i) => i.isButton() && i.customId.startsWith(this.btnPrefix) || i.isStringSelectMenu() && i.customId.startsWith(this.btnPrefix);
986
+ this.ctx.channel.createMessageComponentCollector({
987
+ filter,
988
+ time: this.defaultTimeout
989
+ }).on("collect", async (interaction) => {
990
+ if (interaction.user.id !== this.ctx.userId) {
991
+ ContextAdapter.replyEphemeral(interaction, "⛔ You cannot paginate someone else's leaderboard view!");
992
+ return;
993
+ }
994
+ await interaction.deferUpdate().catch(() => {});
995
+ this.resetTimeout(() => this.destroy());
996
+ if (interaction.isStringSelectMenu()) {
997
+ if (interaction.customId === `${this.btnPrefix}select_metric`) {
998
+ this.selectedMetric = interaction.values[0];
999
+ this.page = 0;
1000
+ }
1001
+ } else if (interaction.isButton()) {
1002
+ const action = interaction.customId.replace(this.btnPrefix, "");
1003
+ if (action === "prev") this.page = Math.max(0, this.page - 1);
1004
+ else if (action === "next") this.page++;
1005
+ }
1006
+ const container = await this.buildContainer();
1007
+ await this.ctx.edit({
1008
+ components: [container],
1009
+ flags: MessageFlags.IsComponentsV2
1010
+ });
1011
+ });
1012
+ }
1013
+ async buildContainer() {
1014
+ const container = this.createContainer();
1015
+ const sym = this.manager.currencySymbol || "🪙";
1016
+ const allEntries = await this.manager.getLeaderboard(this.selectedMetric, 50, this.ctx.guild?.id);
1017
+ const totalPages = Math.ceil(allEntries.length / this.pageSize) || 1;
1018
+ if (this.page >= totalPages) this.page = totalPages - 1;
1019
+ const startIdx = this.page * this.pageSize;
1020
+ const pageEntries = allEntries.slice(startIdx, startIdx + this.pageSize);
1021
+ const metricLabels = {
1022
+ net_worth: "💎 Net Worth",
1023
+ wallet: "💵 Wallet Cash",
1024
+ bank: "🏦 Bank Vault",
1025
+ level: "⭐ Level & XP",
1026
+ streak: "🔥 Daily Streaks"
1027
+ };
1028
+ let content = `## 🏆 Server Economic Leaderboards\n> **Ranking Metric:** \`${metricLabels[this.selectedMetric] || this.selectedMetric}\` • **Page:** \`${this.page + 1}/${totalPages}\`\n\n`;
1029
+ if (pageEntries.length === 0) content += `> *No user rankings recorded yet for this metric.*\n`;
1030
+ else for (const entry of pageEntries) {
1031
+ const medal = entry.rank === 1 ? "🥇" : entry.rank === 2 ? "🥈" : entry.rank === 3 ? "🥉" : `\`#${entry.rank}\``;
1032
+ let valStr = "";
1033
+ if (this.selectedMetric === "level") valStr = `Level ${entry.value.toLocaleString()}`;
1034
+ else if (this.selectedMetric === "streak") valStr = `${entry.value.toLocaleString()} days`;
1035
+ else valStr = `${sym} ${entry.value.toLocaleString()}`;
1036
+ content += `> ${medal} <@${entry.userId}> — **${valStr}**\n`;
1037
+ }
1038
+ container.addTextDisplayComponents((t) => t.setContent(content));
1039
+ const options = [
1040
+ "net_worth",
1041
+ "wallet",
1042
+ "bank",
1043
+ "level",
1044
+ "streak"
1045
+ ].map((m) => new StringSelectMenuOptionBuilder().setLabel(metricLabels[m] || m).setValue(m).setDefault(m === this.selectedMetric));
1046
+ const menuRow = new ActionRowBuilder().addComponents(new StringSelectMenuBuilder().setCustomId(`${this.btnPrefix}select_metric`).setPlaceholder("Select Leaderboard Category...").addOptions(options));
1047
+ container.addActionRowComponents(menuRow);
1048
+ const btnRow = new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId(`${this.btnPrefix}prev`).setLabel("◀ Previous").setStyle(ButtonStyle.Secondary).setDisabled(this.page === 0), new ButtonBuilder().setCustomId(`${this.btnPrefix}next`).setLabel("Next ▶").setStyle(ButtonStyle.Secondary).setDisabled(this.page >= totalPages - 1));
1049
+ container.addActionRowComponents(btnRow);
1050
+ return container;
1051
+ }
1052
+ };
1053
+ // src/ui/EcoVaultUI.ts
1054
+ /**
1055
+ * @m3rcena/eco
1056
+ * Interactive Bank Vault Tiers, Upgrade & Compound Interest UI.
1057
+ *
1058
+ * @copyright 2026 M3rcena Development. All rights reserved.
1059
+ * @license Apache-2.0
1060
+ */
1061
+ var EcoVaultUI = class extends EcoBaseUI {
1062
+ selectedTierNum = 1;
1063
+ btnPrefix;
1064
+ statusMessage = "Select a vault tier below to inspect security specifications and interest yield.";
1065
+ constructor(manager, options) {
1066
+ super(manager, options, "vault_");
1067
+ this.btnPrefix = this.id + "_";
1068
+ }
1069
+ async start() {
1070
+ this.isActive = true;
1071
+ const vault = await this.manager.getUserVault(this.ctx.userId, this.ctx.guild?.id);
1072
+ this.selectedTierNum = vault.currentTier.tier;
1073
+ const container = await this.buildContainer();
1074
+ await this.ctx.sendInitial({
1075
+ components: [container],
1076
+ flags: MessageFlags.IsComponentsV2
1077
+ });
1078
+ this.setupCollector();
1079
+ this.resetTimeout(() => this.destroy());
1080
+ }
1081
+ setupCollector() {
1082
+ if (!this.ctx.channel) return;
1083
+ const filter = (i) => i.isButton() && i.customId.startsWith(this.btnPrefix) || i.isStringSelectMenu() && i.customId.startsWith(this.btnPrefix);
1084
+ this.ctx.channel.createMessageComponentCollector({
1085
+ filter,
1086
+ time: this.defaultTimeout
1087
+ }).on("collect", async (interaction) => {
1088
+ if (interaction.user.id !== this.ctx.userId) {
1089
+ ContextAdapter.replyEphemeral(interaction, "⛔ You cannot manage someone else's bank vault!");
1090
+ return;
1091
+ }
1092
+ await interaction.deferUpdate().catch(() => {});
1093
+ this.resetTimeout(() => this.destroy());
1094
+ if (interaction.isStringSelectMenu()) {
1095
+ if (interaction.customId === `${this.btnPrefix}select_tier`) this.selectedTierNum = parseInt(interaction.values[0], 10);
1096
+ } else if (interaction.isButton()) {
1097
+ const action = interaction.customId.replace(this.btnPrefix, "");
1098
+ if (action === "upgrade") try {
1099
+ const res = await this.manager.upgradeVault(this.ctx.userId, this.ctx.guild?.id);
1100
+ if (res.success) {
1101
+ this.selectedTierNum = res.newTier.tier;
1102
+ this.statusMessage = `🎉 **VAULT UPGRADED!** You are now Tier ${res.newTier.tier} (${res.newTier.name}). New capacity: $${res.newCapacity.toLocaleString()}!`;
1103
+ } else this.statusMessage = `❌ ${res.error}`;
1104
+ } catch (err) {
1105
+ this.statusMessage = `❌ ${err.message}`;
1106
+ }
1107
+ else if (action === "claim_interest") try {
1108
+ const res = await this.manager.claimDailyInterest(this.ctx.userId, this.ctx.guild?.id);
1109
+ if (res.success) this.statusMessage = `💰 **INTEREST DISTRIBUTED!** Added **$${res.interestEarned.toLocaleString()}** (${(res.rate * 100).toFixed(1)}% daily) to your vault savings!`;
1110
+ else this.statusMessage = `⏳ ${res.error}`;
1111
+ } catch (err) {
1112
+ this.statusMessage = `❌ ${err.message}`;
1113
+ }
1114
+ }
1115
+ const container = await this.buildContainer();
1116
+ await this.ctx.edit({
1117
+ components: [container],
1118
+ flags: MessageFlags.IsComponentsV2
1119
+ });
1120
+ });
1121
+ }
1122
+ async buildContainer() {
1123
+ const container = this.createContainer();
1124
+ const sym = this.manager.currencySymbol || "🪙";
1125
+ const userVault = await this.manager.getUserVault(this.ctx.userId, this.ctx.guild?.id);
1126
+ const tiers = this.manager.vault ? this.manager.vault.getVaultTiers() : DEFAULT_VAULT_TIERS;
1127
+ const inspectingTier = tiers.find((t) => t.tier === this.selectedTierNum) || userVault.currentTier;
1128
+ const isCurrent = inspectingTier.tier === userVault.currentTier.tier;
1129
+ userVault.nextTier && (inspectingTier.tier, userVault.nextTier.tier);
1130
+ let content = `## 🏦 Swiss & Orbital Private Vault Showroom\n> **Account Holder:** <@${this.ctx.userId}>\n> **Current Vault Tier:** \`Tier ${userVault.currentTier.tier} — ${userVault.currentTier.name}\`\n> **Savings Vault Balance:** \`${sym} ${userVault.bank.toLocaleString()}\` / \`${sym} ${userVault.capacity.toLocaleString()}\`\n> **Estimated 24h Compound Yield:** \`${sym} ${userVault.dailyInterestAccrual.toLocaleString()} / day\`\n\n> **Status:** ${this.statusMessage}\n\n`;
1131
+ content += `### 🔍 Inspecting: Tier ${inspectingTier.tier} — ${inspectingTier.name} ${isCurrent ? "*(Active Vault)*" : ""}\n> • **Storage Capacity:** \`${sym} ${inspectingTier.capacity.toLocaleString()}\`\n> • **Daily Compound Yield:** \`${(inspectingTier.interestRate * 100).toFixed(1)}% / 24h\`\n> • **Robbery Shield Defense:** \`${(inspectingTier.robberyDefense * 100).toFixed(0)}% deterrence\`\n> • **Upgrade Price:** \`${inspectingTier.upgradeCost > 0 ? `${sym} ${inspectingTier.upgradeCost.toLocaleString()}` : "Included Free"}\`\n`;
1132
+ if (inspectingTier.perks && inspectingTier.perks.length > 0) content += `> • **Special Perks:** ${inspectingTier.perks.join(" • ")}\n`;
1133
+ container.addTextDisplayComponents((t) => t.setContent(content));
1134
+ const options = tiers.map((t) => new StringSelectMenuOptionBuilder().setLabel(`Tier ${t.tier}: ${t.name}`).setValue(String(t.tier)).setDescription(`Cap: ${sym} ${t.capacity.toLocaleString()} | Interest: ${(t.interestRate * 100).toFixed(1)}%`).setDefault(t.tier === this.selectedTierNum));
1135
+ const selectRow = new ActionRowBuilder().addComponents(new StringSelectMenuBuilder().setCustomId(`${this.btnPrefix}select_tier`).setPlaceholder("Select a vault tier to inspect...").addOptions(options));
1136
+ container.addActionRowComponents(selectRow);
1137
+ const canUpgrade = !!userVault.nextTier;
1138
+ const upgradeLabel = canUpgrade ? `⚡ Upgrade to Tier ${userVault.nextTier.tier} (${sym} ${userVault.nextTier.upgradeCost.toLocaleString()})` : "👑 Maximum Vault Tier Reached";
1139
+ const btnRow = new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId(`${this.btnPrefix}upgrade`).setLabel(upgradeLabel).setStyle(canUpgrade ? ButtonStyle.Success : ButtonStyle.Secondary).setDisabled(!canUpgrade), new ButtonBuilder().setCustomId(`${this.btnPrefix}claim_interest`).setLabel(userVault.canClaimInterest ? `💰 Claim Interest (${sym} ${userVault.dailyInterestAccrual.toLocaleString()})` : "⏳ Interest Payout Pending").setStyle(userVault.canClaimInterest ? ButtonStyle.Primary : ButtonStyle.Secondary).setDisabled(!userVault.canClaimInterest));
1140
+ container.addActionRowComponents(btnRow);
1141
+ return container;
1142
+ }
1143
+ };
1144
+ // src/ui/EcoLoanUI.ts
1145
+ /**
1146
+ * @m3rcena/eco
1147
+ * Interactive Bank Loan, Credit Score & Debt Management UI.
1148
+ *
1149
+ * @copyright 2026 M3rcena Development. All rights reserved.
1150
+ * @license Apache-2.0
1151
+ */
1152
+ var EcoLoanUI = class extends EcoBaseUI {
1153
+ selectedTier = "starter";
1154
+ selectedLoanId = null;
1155
+ btnPrefix;
1156
+ statusMessage = "Select a credit package below to inspect terms or choose an active loan to pay down.";
1157
+ constructor(manager, options) {
1158
+ super(manager, options, "loan_");
1159
+ this.btnPrefix = this.id + "_";
1160
+ }
1161
+ async start() {
1162
+ this.isActive = true;
1163
+ const container = await this.buildContainer();
1164
+ await this.ctx.sendInitial({
1165
+ components: [container],
1166
+ flags: MessageFlags.IsComponentsV2
1167
+ });
1168
+ this.setupCollector();
1169
+ this.resetTimeout(() => this.destroy());
1170
+ }
1171
+ setupCollector() {
1172
+ if (!this.ctx.channel) return;
1173
+ const filter = (i) => i.isButton() && i.customId.startsWith(this.btnPrefix) || i.isStringSelectMenu() && i.customId.startsWith(this.btnPrefix);
1174
+ this.ctx.channel.createMessageComponentCollector({
1175
+ filter,
1176
+ time: this.defaultTimeout
1177
+ }).on("collect", async (interaction) => {
1178
+ if (interaction.user.id !== this.ctx.userId) {
1179
+ ContextAdapter.replyEphemeral(interaction, "⛔ You cannot manage someone else's credit lines!");
1180
+ return;
1181
+ }
1182
+ await interaction.deferUpdate().catch(() => {});
1183
+ this.resetTimeout(() => this.destroy());
1184
+ if (interaction.isStringSelectMenu()) {
1185
+ if (interaction.customId === `${this.btnPrefix}select_offer`) this.selectedTier = interaction.values[0];
1186
+ else if (interaction.customId === `${this.btnPrefix}select_loan`) this.selectedLoanId = interaction.values[0];
1187
+ } else if (interaction.isButton()) {
1188
+ const action = interaction.customId.replace(this.btnPrefix, "");
1189
+ if (action === "apply") try {
1190
+ const res = await this.manager.applyLoan(this.ctx.userId, this.selectedTier, this.ctx.guild?.id);
1191
+ if (res.success) this.statusMessage = `✅ **LOAN APPROVED!** Disbursed **$${res.loan.principal.toLocaleString()}** to your cash wallet! (Repay by: <t:${Math.floor(res.loan.dueAt / 1e3)}:R>)`;
1192
+ else this.statusMessage = `❌ Application rejected: ${res.error}`;
1193
+ } catch (err) {
1194
+ this.statusMessage = `❌ ${err.message}`;
1195
+ }
1196
+ else if (action === "repay_all" || action === "repay_half") {
1197
+ const activeLoan = (await this.manager.getUserLoans(this.ctx.userId, this.ctx.guild?.id)).find((l) => this.selectedLoanId ? l.id === this.selectedLoanId : l.status !== "paid");
1198
+ if (!activeLoan) {
1199
+ ContextAdapter.replyEphemeral(interaction, "❌ You have no active debts to pay!");
1200
+ return;
1201
+ }
1202
+ const payAmount = action === "repay_all" ? "all" : Math.ceil(activeLoan.remainingDue / 2);
1203
+ try {
1204
+ const res = await this.manager.repayLoan(this.ctx.userId, activeLoan.id, payAmount, this.ctx.guild?.id);
1205
+ if (res.success) {
1206
+ if (res.isFullyPaid) this.statusMessage = `🎉 **DEBT PAID IN FULL!** Succeeded in settling debt. Credit score boosted to **${res.newCreditScore}**!`;
1207
+ else this.statusMessage = `💵 Paid **$${res.amountPaid.toLocaleString()}** toward debt. Remaining: **$${res.remainingDue.toLocaleString()}**`;
1208
+ } else this.statusMessage = `❌ ${res.error}`;
1209
+ } catch (err) {
1210
+ this.statusMessage = `❌ ${err.message}`;
1211
+ }
1212
+ }
1213
+ }
1214
+ const container = await this.buildContainer();
1215
+ await this.ctx.edit({
1216
+ components: [container],
1217
+ flags: MessageFlags.IsComponentsV2
1218
+ });
1219
+ });
1220
+ }
1221
+ async buildContainer() {
1222
+ const container = this.createContainer();
1223
+ const sym = this.manager.currencySymbol || "🪙";
1224
+ const credit = await this.manager.getCreditScore(this.ctx.userId, this.ctx.guild?.id);
1225
+ const loans = await this.manager.getUserLoans(this.ctx.userId, this.ctx.guild?.id);
1226
+ const offers = this.manager.loans ? this.manager.loans.getOffers() : LOAN_CATALOG;
1227
+ const selectedOffer = offers.find((o) => o.tier === this.selectedTier) || offers[0];
1228
+ const activeLoans = loans.filter((l) => l.status === "active" || l.status === "defaulted");
1229
+ const totalDebt = activeLoans.reduce((sum, l) => sum + l.remainingDue, 0);
1230
+ let content = `## 🏦 Global Commercial Credit Bureau & Loan Broker\n> **Account:** <@${this.ctx.userId}> • **FICO Credit Rating:** \`${{
1231
+ EXCELLENT: "🟢 800-850 (EXCELLENT)",
1232
+ VERY_GOOD: "🟢 740-799 (VERY GOOD)",
1233
+ GOOD: "🟡 670-739 (GOOD)",
1234
+ FAIR: "🟠 580-669 (FAIR)",
1235
+ POOR: "🔴 300-579 (POOR - HIGH RISK)"
1236
+ }[credit.rating] || credit.rating}\` (\`${credit.score} Score\`)\n> **Total Active Debt:** \`${sym} ${totalDebt.toLocaleString()}\` • **Loans Repaid:** \`${credit.loansRepaid}\` • **Defaults:** \`${credit.defaultsCount}\`\n\n> **Status:** ${this.statusMessage}\n\n`;
1237
+ if (activeLoans.length > 0) {
1238
+ content += `### 💳 Outstanding Debts (${activeLoans.length}/2 Active):\n`;
1239
+ for (const l of activeLoans) {
1240
+ const tag = l.status === "defaulted" ? "🚨 **[OVERDUE DEFAULT]**" : `⏳ Due: <t:${Math.floor(l.dueAt / 1e3)}:R>`;
1241
+ content += `> • **${l.name}**: Remaining: \`${sym} ${l.remainingDue.toLocaleString()}\` / \`${sym} ${l.totalDue.toLocaleString()}\` — ${tag}\n`;
1242
+ }
1243
+ content += `\n`;
1244
+ }
1245
+ content += `### 📋 Inspecting Offer: ${selectedOffer.name}\n> • **Principal Cash Advance:** \`${sym} ${selectedOffer.principal.toLocaleString()}\`\n> • **Fixed Interest:** \`${(selectedOffer.interestRate * 100).toFixed(1)}%\` (Total Due: \`${sym} ${selectedOffer.totalDue.toLocaleString()}\`)\n> • **Repayment Window:** \`${selectedOffer.durationHours} hours\`\n> • **Required Credit Score:** \`${selectedOffer.minCreditScore}+\` ${credit.score >= selectedOffer.minCreditScore ? "*(Eligible)*" : "*(Score Too Low)*"}\n> • **Description:** *${selectedOffer.description}*\n`;
1246
+ container.addTextDisplayComponents((t) => t.setContent(content));
1247
+ const options = offers.map((o) => new StringSelectMenuOptionBuilder().setLabel(`${o.name} (${sym} ${o.principal.toLocaleString()})`).setValue(o.tier).setDescription(`Rate: ${(o.interestRate * 100).toFixed(1)}% | Min Score: ${o.minCreditScore}`).setDefault(o.tier === this.selectedTier));
1248
+ const offerRow = new ActionRowBuilder().addComponents(new StringSelectMenuBuilder().setCustomId(`${this.btnPrefix}select_offer`).setPlaceholder("Select a credit advance package...").addOptions(options));
1249
+ container.addActionRowComponents(offerRow);
1250
+ const canApply = credit.score >= selectedOffer.minCreditScore && activeLoans.length < 2 && !activeLoans.some((l) => l.status === "defaulted");
1251
+ const hasDebt = totalDebt > 0;
1252
+ const btnRow = new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId(`${this.btnPrefix}apply`).setLabel(`📝 Apply for ${selectedOffer.name}`).setStyle(canApply ? ButtonStyle.Success : ButtonStyle.Secondary).setDisabled(!canApply), new ButtonBuilder().setCustomId(`${this.btnPrefix}repay_half`).setLabel("💵 Pay 50% Debt").setStyle(ButtonStyle.Primary).setDisabled(!hasDebt), new ButtonBuilder().setCustomId(`${this.btnPrefix}repay_all`).setLabel("🎉 Settle Full Debt").setStyle(ButtonStyle.Success).setDisabled(!hasDebt));
1253
+ container.addActionRowComponents(btnRow);
1254
+ return container;
1255
+ }
1256
+ };
1257
+ // src/ui/EcoBillsUI.ts
1258
+ /**
1259
+ * @m3rcena/eco
1260
+ * Interactive Municipal Bills, Wealth Taxes & Audit Desk UI.
1261
+ *
1262
+ * @copyright 2026 M3rcena Development. All rights reserved.
1263
+ * @license Apache-2.0
1264
+ */
1265
+ var EcoBillsUI = class extends EcoBaseUI {
1266
+ selectedBillId = null;
1267
+ btnPrefix;
1268
+ statusMessage = "Review municipal bills and wealth taxes below. Settle on time to protect your credit score!";
1269
+ constructor(manager, options) {
1270
+ super(manager, options, "bills_");
1271
+ this.btnPrefix = this.id + "_";
1272
+ }
1273
+ async start() {
1274
+ this.isActive = true;
1275
+ if (this.manager.taxes && typeof this.manager.taxes.generateBillsIfDue === "function") await this.manager.taxes.generateBillsIfDue(this.ctx.userId, this.ctx.guild?.id);
1276
+ const container = await this.buildContainer();
1277
+ await this.ctx.sendInitial({
1278
+ components: [container],
1279
+ flags: MessageFlags.IsComponentsV2
1280
+ });
1281
+ this.setupCollector();
1282
+ this.resetTimeout(() => this.destroy());
1283
+ }
1284
+ setupCollector() {
1285
+ if (!this.ctx.channel) return;
1286
+ const filter = (i) => i.isButton() && i.customId.startsWith(this.btnPrefix) || i.isStringSelectMenu() && i.customId.startsWith(this.btnPrefix);
1287
+ this.ctx.channel.createMessageComponentCollector({
1288
+ filter,
1289
+ time: this.defaultTimeout
1290
+ }).on("collect", async (interaction) => {
1291
+ if (interaction.user.id !== this.ctx.userId) {
1292
+ ContextAdapter.replyEphemeral(interaction, "⛔ You cannot manage someone else's municipal tax desk!");
1293
+ return;
1294
+ }
1295
+ await interaction.deferUpdate().catch(() => {});
1296
+ this.resetTimeout(() => this.destroy());
1297
+ if (interaction.isStringSelectMenu()) {
1298
+ if (interaction.customId === `${this.btnPrefix}select_bill`) this.selectedBillId = interaction.values[0];
1299
+ } else if (interaction.isButton()) {
1300
+ const action = interaction.customId.replace(this.btnPrefix, "");
1301
+ if (action === "pay_selected") {
1302
+ if (!this.selectedBillId) {
1303
+ ContextAdapter.replyEphemeral(interaction, "❌ Please select a bill from the menu first.");
1304
+ return;
1305
+ }
1306
+ try {
1307
+ const res = await this.manager.payBill(this.ctx.userId, this.selectedBillId, this.ctx.guild?.id);
1308
+ if (res.success) this.statusMessage = `✅ Paid **${res.bill?.title}** ($${res.amountPaid.toLocaleString()})! Credit score increased to **${res.newCreditScore}**!`;
1309
+ else this.statusMessage = `❌ ${res.error}`;
1310
+ } catch (err) {
1311
+ this.statusMessage = `❌ ${err.message}`;
1312
+ }
1313
+ } else if (action === "pay_all") try {
1314
+ const res = await this.manager.payAllBills(this.ctx.userId, this.ctx.guild?.id);
1315
+ if (res.success) this.statusMessage = `🏛️ **TAX COMPLIANCE ACHIEVED!** Settled ${res.paidCount} bills ($${res.totalAmountPaid.toLocaleString()})! Credit score boosted to **${res.newCreditScore}**!`;
1316
+ else this.statusMessage = `❌ ${res.error}`;
1317
+ } catch (err) {
1318
+ this.statusMessage = `❌ ${err.message}`;
1319
+ }
1320
+ else if (action === "evade") try {
1321
+ const res = await this.manager.attemptTaxEvasion(this.ctx.userId, this.ctx.guild?.id);
1322
+ this.statusMessage = res.auditDetails;
1323
+ } catch (err) {
1324
+ this.statusMessage = `❌ ${err.message}`;
1325
+ }
1326
+ }
1327
+ const container = await this.buildContainer();
1328
+ await this.ctx.edit({
1329
+ components: [container],
1330
+ flags: MessageFlags.IsComponentsV2
1331
+ });
1332
+ });
1333
+ }
1334
+ async buildContainer() {
1335
+ const container = this.createContainer();
1336
+ const sym = this.manager.currencySymbol || "🪙";
1337
+ const bills = await this.manager.getPendingBills(this.ctx.userId, this.ctx.guild?.id);
1338
+ const totalBilled = bills.reduce((sum, b) => sum + b.amount, 0);
1339
+ const credit = await this.manager.getCreditScore(this.ctx.userId, this.ctx.guild?.id);
1340
+ let content = `## 🏛️ Municipal Treasury, Utilities & Tax Compliance\n> **Citizen:** <@${this.ctx.userId}> • **Credit Rating:** \`${credit.rating}\` (\`${credit.score} pts\`)\n> **Total Pending Obligations:** \`${sym} ${totalBilled.toLocaleString()}\` across \`${bills.length} bills\`\n\n> **Status:** ${this.statusMessage}\n\n`;
1341
+ if (bills.length === 0) content += `> ✨ **ALL OBLIGATIONS SETTLED!** You are in 100% tax and utility compliance. No pending fees!\n`;
1342
+ else {
1343
+ content += `### 📄 Pending Notices:\n`;
1344
+ for (const b of bills) {
1345
+ const isSel = b.id === this.selectedBillId ? "👉 " : "• ";
1346
+ content += `> ${isSel}**${b.title}**: \`${sym} ${b.amount.toLocaleString()}\` (Due: <t:${Math.floor(b.dueAt / 1e3)}:R>)\n`;
1347
+ }
1348
+ }
1349
+ container.addTextDisplayComponents((t) => t.setContent(content));
1350
+ if (bills.length > 0) {
1351
+ const options = bills.map((b) => new StringSelectMenuOptionBuilder().setLabel(`${b.title.slice(0, 50)}`).setValue(b.id).setDescription(`${sym} ${b.amount.toLocaleString()}`).setDefault(b.id === this.selectedBillId));
1352
+ const billRow = new ActionRowBuilder().addComponents(new StringSelectMenuBuilder().setCustomId(`${this.btnPrefix}select_bill`).setPlaceholder("Select a bill to settle...").addOptions(options));
1353
+ container.addActionRowComponents(billRow);
1354
+ const hasTax = bills.some((b) => b.type === "wealth_tax");
1355
+ const btnRow = new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId(`${this.btnPrefix}pay_selected`).setLabel("Pay Selected Bill").setStyle(ButtonStyle.Primary).setDisabled(!this.selectedBillId), new ButtonBuilder().setCustomId(`${this.btnPrefix}pay_all`).setLabel(`🏛️ Settle All (${sym} ${totalBilled.toLocaleString()})`).setStyle(ButtonStyle.Success), new ButtonBuilder().setCustomId(`${this.btnPrefix}evade`).setLabel("🎲 Offshore Shell Evasion").setStyle(ButtonStyle.Danger).setDisabled(!hasTax));
1356
+ container.addActionRowComponents(btnRow);
1357
+ }
1358
+ return container;
1359
+ }
1360
+ };
1361
+ // src/index.ts
1362
+ /**
1363
+ * @m3rcena/eco
1364
+ * The Ultimate High-Performance Economy, Progression & Casino Engine for Discord.js bots.
1365
+ *
1366
+ * @copyright 2026 M3rcena Development. All rights reserved.
1367
+ * @license Apache-2.0
1368
+ */
1369
+ export { BankManager, BusinessManager, CRATE_PRICES, CareerManager, CasinoManager, ContextAdapter, CrateManager, DEFAULT_BUSINESS_CATALOG, DEFAULT_HEIST_TARGETS, DEFAULT_JOBS, DEFAULT_PETS, DEFAULT_SHOP_ITEMS, DEFAULT_STOCKS, DEFAULT_VAULT_TIERS, ECO_VERSION, EcoBaseUI, EcoBillsUI, EcoBlackjackUI, EcoBridgeRouter, EcoCrateUI, EcoDatabaseHandler, EcoHeistUI, EcoLeaderboardUI, EcoLoanUI, EcoManager, EcoProfileUI, EcoShopUI, EcoStockMarketUI, EcoVaultUI, EcoWekyConnector, HeistManager, JsonEcoStorage, LOAN_CATALOG, LeaderboardManager, LoanManager, M3rcenaBridge, MemoryEcoStorage, PRISMA_ECO_MODEL, PetManager, PrismaEcoStorage, RenderBankCard, RenderMarketChart, RewardManager, ShopManager, SqliteEcoStorage, StockMarketManager, TaxManager, VaultManager, applyPrismaEcoModel, boxConsole, checkPackageUpdates, compareSemver, createDefaultAccount, fetchLatestVersion, findProjectRoot, generateTsConfigPaths, getAllPackageResolutions, hostEco, isTypeScriptRuntime, loadResolverConfig, printResolutionSummary, resolveEcoStorage, resolvePackage, stripAnsi };
1370
+
1371
+ //# sourceMappingURL=index.mjs.map