diff --git a/SUGERENCIAS_Y_MEJORAS.md b/SUGERENCIAS_Y_MEJORAS.md new file mode 100644 index 0000000..5fd0658 --- /dev/null +++ b/SUGERENCIAS_Y_MEJORAS.md @@ -0,0 +1,617 @@ +# 🎯 Análisis Completo y Sugerencias de Mejora + +## 📊 Análisis del Proyecto + +Después de analizar todo tu código, he identificado que tienes un **excelente sistema base** con: + +✅ Sistema de economía robusto (items, wallet, inventario) +✅ Sistema de minijuegos completo (mina, pesca, pelea, granja) +✅ Sistema de combate con equipamiento y stats +✅ Sistema de crafteo y fundición +✅ Sistema de mutaciones para items +✅ DisplayComponents avanzado con editor visual +✅ Sistema de cooldowns y progresión +✅ Sistema de alianzas con leaderboards + +--- + +## 🚀 Sugerencias de Nuevas Funcionalidades + +### 1. 🏆 **Sistema de Logros/Achievements** ⭐⭐⭐⭐⭐ + +**Por qué es importante:** Los logros mantienen a los jugadores enganchados con objetivos claros. + +**Implementación sugerida:** + +```typescript +// prisma/schema.prisma +model Achievement { + id String @id @default(cuid()) + key String // "first_mine", "craft_10_items", "defeat_100_mobs" + name String + description String + icon String? + category String // "mining", "crafting", "combat", "economy" + + // Requisitos para desbloquear (JSON flexible) + requirements Json // { type: "mine_count", value: 100 } + + // Recompensas al desbloquear + rewards Json? // { coins: 500, items: [...], title: "..." } + + guildId String? + guild Guild? @relation(fields: [guildId], references: [id]) + + // Logros desbloqueados por usuarios + unlocked PlayerAchievement[] + + hidden Boolean @default(false) // logros secretos + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([guildId, key]) +} + +model PlayerAchievement { + id String @id @default(cuid()) + userId String + guildId String + achievementId String + + user User @relation(fields: [userId], references: [id]) + guild Guild @relation(fields: [guildId], references: [id]) + achievement Achievement @relation(fields: [achievementId], references: [id]) + + unlockedAt DateTime @default(now()) + metadata Json? // datos extra como progreso + + @@unique([userId, guildId, achievementId]) + @@index([userId, guildId]) +} +``` + +**Comandos sugeridos:** +- `!logros` - Ver tus logros desbloqueados y progreso +- `!logros @usuario` - Ver logros de otro usuario +- `!logro-crear ` - Crear nuevo logro (admin) +- `!ranking-logros` - Ver quién tiene más logros + +**Ejemplos de logros:** +- 🎖️ **Primera Mina**: Mina por primera vez +- ⛏️ **Minero Novato**: Mina 50 veces +- ⛏️ **Minero Veterano**: Mina 500 veces +- 🎣 **Pescador Experto**: Pesca 100 veces +- ⚔️ **Cazador de Monstruos**: Derrota 100 mobs +- 💰 **Millonario**: Acumula 1,000,000 monedas +- 🛠️ **Maestro Craftero**: Craftea 100 items +- 📦 **Coleccionista**: Ten 50 items únicos +- 🗡️ **Arsenal Completo**: Equipa arma, armadura y capa legendarias + +--- + +### 2. 📊 **Sistema de Estadísticas Detalladas** ⭐⭐⭐⭐ + +**Por qué es importante:** A los jugadores les gusta ver su progreso en números. + +**Implementación sugerida:** + +```typescript +// prisma/schema.prisma +model PlayerStats { + id String @id @default(cuid()) + userId String + guildId String + + // Stats de minijuegos + minesCompleted Int @default(0) + fishingCompleted Int @default(0) + fightsCompleted Int @default(0) + farmsCompleted Int @default(0) + + // Stats de combate + mobsDefeated Int @default(0) + damageDealt Int @default(0) + damageTaken Int @default(0) + + // Stats de economía + totalCoinsEarned Int @default(0) + totalCoinsSpent Int @default(0) + itemsCrafted Int @default(0) + itemsSmelted Int @default(0) + + // Stats de items + chestsOpened Int @default(0) + itemsConsumed Int @default(0) + + // Récords personales + highestDamageDealt Int @default(0) + longestWinStreak Int @default(0) + + user User @relation(fields: [userId], references: [id]) + guild Guild @relation(fields: [guildId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([userId, guildId]) +} +``` + +**Comando sugerido:** +``` +!stats [@usuario] +``` + +Muestra: +- Total de actividades realizadas +- Mobs derrotados +- Monedas ganadas/gastadas +- Items crafteados +- Récords personales + +--- + +### 3. 🎁 **Sistema de Misiones Diarias/Semanales** ⭐⭐⭐⭐⭐ + +**Por qué es importante:** Mantiene a los jugadores volviendo cada día. + +**Implementación sugerida:** + +```typescript +// prisma/schema.prisma +model Quest { + id String @id @default(cuid()) + key String + name String + description String + icon String? + + // Tipo de misión + type String // "daily", "weekly", "event" + category String // "mining", "combat", "economy" + + // Requisitos + requirements Json // { type: "mine", count: 10 } + + // Recompensas + rewards Json // { coins: 500, items: [...], xp: 100 } + + // Disponibilidad + startAt DateTime? + endAt DateTime? + + guildId String? + guild Guild? @relation(fields: [guildId], references: [id]) + + progress QuestProgress[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([guildId, key]) +} + +model QuestProgress { + id String @id @default(cuid()) + userId String + guildId String + questId String + + progress Int @default(0) // progreso actual + completed Boolean @default(false) + claimed Boolean @default(false) // si ya reclamó recompensa + + user User @relation(fields: [userId], references: [id]) + guild Guild @relation(fields: [guildId], references: [id]) + quest Quest @relation(fields: [questId], references: [id]) + + completedAt DateTime? + claimedAt DateTime? + expiresAt DateTime? // para misiones diarias/semanales + + @@unique([userId, guildId, questId]) + @@index([userId, guildId]) +} +``` + +**Comandos sugeridos:** +- `!misiones` - Ver misiones disponibles y progreso +- `!mision-reclamar ` - Reclamar recompensa de misión completada +- `!mision-crear ` - Crear nueva misión (admin) + +**Ejemplos de misiones diarias:** +- ⛏️ Mina 10 veces (Recompensa: 500 monedas) +- 🎣 Pesca 5 veces (Recompensa: 3 cañas) +- ⚔️ Derrota 15 mobs (Recompensa: poción de vida) +- 🛒 Compra 3 items de la tienda (Recompensa: 10% descuento) +- 🛠️ Craftea 5 items (Recompensa: materiales extra) + +--- + +### 4. 🏪 **Comando de Tienda Mejorado** ⭐⭐⭐⭐⭐ + +**Lo que falta actualmente:** +- Solo tienes `!comprar ` que requiere saber el ID +- No hay forma visual de ver la tienda +- No hay categorías ni filtros +- No hay vista previa de items + +**Voy a crear un comando completo con DisplayComponents** ⬇️ + +--- + +### 5. 🎲 **Sistema de Eventos Temporales** ⭐⭐⭐⭐ + +**Por qué es importante:** Crea urgencia y mantiene el contenido fresco. + +**Implementación sugerida:** + +```typescript +// prisma/schema.prisma +model Event { + id String @id @default(cuid()) + key String + name String + description String + icon String? + + // Configuración del evento + config Json? // multiplicadores, drops especiales, etc. + + // Items/áreas/mobs específicos del evento + specialItems String[] // keys de items que solo aparecen en evento + + // Fechas + startAt DateTime + endAt DateTime + + guildId String? + guild Guild? @relation(fields: [guildId], references: [id]) + + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([guildId, key]) +} +``` + +**Ejemplos de eventos:** +- 🎃 **Halloween:** Mobs especiales, items de Halloween, drops x2 +- 🎄 **Navidad:** Cofres navideños, items temáticos, monedas x1.5 +- 🐰 **Pascua:** Huevos de Pascua escondidos, mobs conejos +- 💘 **San Valentín:** Items de amor, crafteos especiales + +**Comando sugerido:** +``` +!eventos +``` +Muestra eventos activos con tiempo restante y recompensas especiales. + +--- + +### 6. 🔄 **Sistema de Intercambio entre Jugadores** ⭐⭐⭐⭐ + +**Por qué es importante:** Fomenta la economía entre jugadores. + +**Implementación sugerida:** + +```typescript +// prisma/schema.prisma +model Trade { + id String @id @default(cuid()) + + // Jugador que inicia el trade + initiatorId String + initiator User @relation("TradeInitiator", fields: [initiatorId], references: [id]) + + // Jugador que recibe la oferta + targetId String + target User @relation("TradeTarget", fields: [targetId], references: [id]) + + guildId String + guild Guild @relation(fields: [guildId], references: [id]) + + // Ofertas de ambos jugadores (JSON) + initiatorOffer Json // { coins: 100, items: [{ key, qty }] } + targetOffer Json // { coins: 50, items: [{ key, qty }] } + + status String // "pending", "accepted", "rejected", "expired", "completed" + + createdAt DateTime @default(now()) + expiresAt DateTime + completedAt DateTime? + + @@index([initiatorId, targetId, guildId]) +} +``` + +**Comandos sugeridos:** +- `!tradear @usuario` - Iniciar intercambio +- `!trade-ofrecer ` - Añadir a tu oferta +- `!trade-aceptar` - Aceptar intercambio +- `!trade-cancelar` - Cancelar intercambio + +--- + +### 7. 🏘️ **Sistema de Guilds/Clanes** ⭐⭐⭐⭐⭐ + +**Por qué es importante:** Fomenta el trabajo en equipo y competencia. + +**Implementación sugerida:** + +```typescript +// prisma/schema.prisma +model PlayerGuild { + id String @id @default(cuid()) + name String + tag String // [TAG] + description String? + icon String? + + leaderId String + leader User @relation(fields: [leaderId], references: [id]) + + guildId String // Discord guild + discordGuild Guild @relation(fields: [guildId], references: [id]) + + // Stats del clan + level Int @default(1) + xp Int @default(0) + + // Recursos del clan + treasury Int @default(0) // monedas compartidas + + members PlayerGuildMember[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([guildId, tag]) +} + +model PlayerGuildMember { + id String @id @default(cuid()) + userId String + playerGuildId String + + user User @relation(fields: [userId], references: [id]) + playerGuild PlayerGuild @relation(fields: [playerGuildId], references: [id]) + + rank String @default("member") // "leader", "officer", "member" + contribution Int @default(0) // contribución total + + joinedAt DateTime @default(now()) + + @@unique([userId, playerGuildId]) +} +``` + +**Comandos sugeridos:** +- `!clan-crear ` - Crear clan +- `!clan-info [@clan]` - Ver info del clan +- `!clan-invitar @usuario` - Invitar a clan +- `!clan-donar ` - Donar al tesoro +- `!clan-ranking` - Ver ranking de clanes +- `!clan-buffs` - Buffs activos del clan + +**Beneficios de clanes:** +- Buffs compartidos (XP+10%, drops+5%, etc.) +- Almacén compartido +- Misiones de clan con recompensas grupales +- Guerras entre clanes +- Ranking de clanes + +--- + +### 8. 🎰 **Sistema de Ruleta/Lotería** ⭐⭐⭐ + +**Por qué es importante:** Da emoción y oportunidades de ganar grande. + +**Comando sugerido:** +``` +!ruleta +``` + +- Cuesta monedas +- Puede dar: monedas x2, x5, x10, items raros, o perder todo +- Cooldown de 1 hora + +--- + +### 9. 🏠 **Sistema de Viviendas/Bases** ⭐⭐⭐⭐ + +**Por qué es importante:** Personalización y progresión a largo plazo. + +**Características:** +- Cada jugador puede tener una casa +- Mejoras de casa (nivel 1-10) +- Almacén extra por nivel +- Decoraciones +- Buffs pasivos (+5% XP, -10% cooldowns, etc.) +- Costo de mantenimiento semanal + +--- + +### 10. 🔥 **Sistema de Racha (Streaks)** ⭐⭐⭐⭐ + +**Por qué es importante:** Incentiva jugar todos los días. + +**Implementación:** +```typescript +model PlayerStreak { + id String @id @default(cuid()) + userId String + guildId String + + currentStreak Int @default(0) + longestStreak Int @default(0) + lastActiveDate DateTime + + user User @relation(fields: [userId], references: [id]) + guild Guild @relation(fields: [guildId], references: [id]) + + @@unique([userId, guildId]) +} +``` + +**Recompensas por racha:** +- Día 1: 100 monedas +- Día 3: 300 monedas + cofre básico +- Día 7: 1000 monedas + cofre raro +- Día 14: 3000 monedas + cofre épico +- Día 30: 10000 monedas + item legendario + +--- + +## 🐛 Bugs y Mejoras Técnicas Detectadas + +### 1. **Sistema de Cooldowns** +**Problema:** Los cooldowns están dispersos por actividad. +**Mejora:** Centralizar en una función helper que muestre todos los cooldowns activos de un usuario. + +```typescript +// src/game/cooldowns/service.ts +export async function getUserActiveCooldowns(userId: string, guildId: string) { + const cds = await prisma.actionCooldown.findMany({ + where: { userId, guildId, until: { gt: new Date() } }, + orderBy: { until: 'asc' } + }); + + return cds.map(cd => ({ + key: cd.key, + remaining: Math.ceil((cd.until.getTime() - Date.now()) / 1000), + expiresAt: cd.until + })); +} +``` + +### 2. **Validación de Props en Items** +**Mejora:** Añadir validación TypeScript más estricta para props de items. + +```typescript +// src/game/economy/validators.ts +export function validateItemProps(props: any): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + if (props.tool) { + if (!['pickaxe', 'rod', 'sword', 'bow', 'halberd', 'net'].includes(props.tool.type)) { + errors.push(`Invalid tool type: ${props.tool.type}`); + } + if (props.tool.tier && (props.tool.tier < 1 || props.tool.tier > 10)) { + errors.push(`Tool tier must be between 1-10`); + } + } + + if (props.damage && props.damage < 0) { + errors.push(`Damage cannot be negative`); + } + + // ... más validaciones + + return { valid: errors.length === 0, errors }; +} +``` + +### 3. **Sistema de Paginación Mejorado** +**Mejora:** Crear componente reutilizable para paginación en todos los comandos. + +```typescript +// src/core/lib/pagination.ts +export function paginate(items: T[], page: number, perPage: number) { + const totalPages = Math.max(1, Math.ceil(items.length / perPage)); + const safePage = Math.min(Math.max(1, page), totalPages); + const start = (safePage - 1) * perPage; + const end = start + perPage; + + return { + items: items.slice(start, end), + page: safePage, + totalPages, + hasNext: safePage < totalPages, + hasPrev: safePage > 1, + total: items.length + }; +} +``` + +### 4. **Logs y Auditoría** +**Mejora:** Añadir sistema de logs para operaciones importantes. + +```typescript +// prisma/schema.prisma +model AuditLog { + id String @id @default(cuid()) + userId String + guildId String + action String // "buy", "craft", "trade", "equip", etc. + target String? // ID del item/mob/área afectado + details Json? // detalles adicionales + + user User @relation(fields: [userId], references: [id]) + guild Guild @relation(fields: [guildId], references: [id]) + + createdAt DateTime @default(now()) + + @@index([userId, guildId]) + @@index([action]) + @@index([createdAt]) +} +``` + +--- + +## 📝 Prioridades Recomendadas + +### Alta Prioridad (Implementar Ya) +1. ✅ **Comando de Tienda con DisplayComponents** (lo voy a crear) +2. 🏆 **Sistema de Logros** +3. 🎁 **Misiones Diarias** +4. 🔥 **Sistema de Rachas** + +### Media Prioridad (Próximos Sprints) +5. 📊 **Estadísticas Detalladas** +6. 🏘️ **Sistema de Clanes** +7. 🎲 **Eventos Temporales** + +### Baja Prioridad (Largo Plazo) +8. 🔄 **Intercambio entre Jugadores** +9. 🏠 **Sistema de Viviendas** +10. 🎰 **Ruleta/Lotería** + +--- + +## 🎨 Mejoras de UX/UI + +### 1. Mensajes más Visuales +Usar más DisplayComponents en lugar de texto plano: +- `!inventario` → DisplayComponents con imágenes +- `!player` → DisplayComponents con stats visuales +- `!tienda` → DisplayComponents con preview de items + +### 2. Botones Interactivos +Añadir botones en lugar de comandos: +- `!mina` → Añadir botones "Minar Otra Vez", "Ver Inventario" +- `!pelear` → Añadir botones "Pelear de Nuevo", "Equipar Mejor Arma" +- `!tienda` → Añadir botones de compra directa + +### 3. Confirmaciones +Añadir confirmaciones para acciones importantes: +- Comprar items caros +- Fundir items +- Eliminar items + +--- + +## 🎯 Conclusión + +Tu bot tiene una **base excelente y muy completa**. Las sugerencias principales son: + +1. **Más engagement:** Logros, misiones, rachas +2. **Más social:** Clanes, trades, rankings +3. **Más visual:** DisplayComponents en todos lados +4. **Más feedback:** Stats, logs, notificaciones + +Ahora voy a crear el **comando de tienda completo con DisplayComponents** 🚀 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ffe159c..86eeca6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -52,6 +52,15 @@ model Guild { ActionCooldown ActionCooldown[] SmeltJob SmeltJob[] ScheduledMobAttack ScheduledMobAttack[] + + // Nuevas relaciones para sistemas de engagement + Achievement Achievement[] + PlayerAchievement PlayerAchievement[] + Quest Quest[] + QuestProgress QuestProgress[] + PlayerStats PlayerStats[] + PlayerStreak PlayerStreak[] + AuditLog AuditLog[] } /** @@ -80,6 +89,13 @@ model User { ActionCooldown ActionCooldown[] SmeltJob SmeltJob[] ScheduledMobAttack ScheduledMobAttack[] + + // Nuevas relaciones para sistemas de engagement + PlayerAchievement PlayerAchievement[] + QuestProgress QuestProgress[] + PlayerStats PlayerStats[] + PlayerStreak PlayerStreak[] + AuditLog AuditLog[] } /** @@ -743,3 +759,230 @@ model ScheduledMobAttack { @@index([scheduleAt]) @@index([userId, guildId]) } + +/** + * ----------------------------------------------------------------------------- + * Sistema de Logros (Achievements) + * ----------------------------------------------------------------------------- + */ +model Achievement { + id String @id @default(cuid()) + key String + name String + description String + icon String? + category String // "mining", "crafting", "combat", "economy", "exploration" + + // Requisitos para desbloquear (JSON flexible) + requirements Json // { type: "mine_count", value: 100 } + + // Recompensas al desbloquear + rewards Json? // { coins: 500, items: [...], title: "..." } + + guildId String? + guild Guild? @relation(fields: [guildId], references: [id]) + + // Logros desbloqueados por usuarios + unlocked PlayerAchievement[] + + hidden Boolean @default(false) // logros secretos + points Int @default(10) // puntos que otorga el logro + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([guildId, key]) + @@index([guildId]) +} + +model PlayerAchievement { + id String @id @default(cuid()) + userId String + guildId String + achievementId String + + user User @relation(fields: [userId], references: [id]) + guild Guild @relation(fields: [guildId], references: [id]) + achievement Achievement @relation(fields: [achievementId], references: [id]) + + progress Int @default(0) // progreso actual hacia el logro + unlockedAt DateTime? // null si aún no está desbloqueado + notified Boolean @default(false) // si ya se notificó al usuario + metadata Json? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([userId, guildId, achievementId]) + @@index([userId, guildId]) +} + +/** + * ----------------------------------------------------------------------------- + * Sistema de Misiones (Quests) + * ----------------------------------------------------------------------------- + */ +model Quest { + id String @id @default(cuid()) + key String + name String + description String + icon String? + + // Tipo de misión + type String // "daily", "weekly", "event", "permanent" + category String // "mining", "combat", "economy", "exploration" + + // Requisitos + requirements Json // { type: "mine", count: 10 } + + // Recompensas + rewards Json // { coins: 500, items: [...], xp: 100 } + + // Disponibilidad + startAt DateTime? + endAt DateTime? + + guildId String? + guild Guild? @relation(fields: [guildId], references: [id]) + + progress QuestProgress[] + + active Boolean @default(true) + repeatable Boolean @default(false) // si se puede repetir después de completar + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([guildId, key]) + @@index([guildId]) + @@index([type]) +} + +model QuestProgress { + id String @id @default(cuid()) + userId String + guildId String + questId String + + progress Int @default(0) // progreso actual + completed Boolean @default(false) + claimed Boolean @default(false) // si ya reclamó recompensa + + user User @relation(fields: [userId], references: [id]) + guild Guild @relation(fields: [guildId], references: [id]) + quest Quest @relation(fields: [questId], references: [id]) + + completedAt DateTime? + claimedAt DateTime? + expiresAt DateTime? // para misiones diarias/semanales + metadata Json? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([userId, guildId, questId, expiresAt]) + @@index([userId, guildId]) + @@index([questId]) +} + +/** + * ----------------------------------------------------------------------------- + * Sistema de Estadísticas del Jugador + * ----------------------------------------------------------------------------- + */ +model PlayerStats { + id String @id @default(cuid()) + userId String + guildId String + + // Stats de minijuegos + minesCompleted Int @default(0) + fishingCompleted Int @default(0) + fightsCompleted Int @default(0) + farmsCompleted Int @default(0) + + // Stats de combate + mobsDefeated Int @default(0) + damageDealt Int @default(0) + damageTaken Int @default(0) + timesDefeated Int @default(0) + + // Stats de economía + totalCoinsEarned Int @default(0) + totalCoinsSpent Int @default(0) + itemsCrafted Int @default(0) + itemsSmelted Int @default(0) + itemsPurchased Int @default(0) + + // Stats de items + chestsOpened Int @default(0) + itemsConsumed Int @default(0) + itemsEquipped Int @default(0) + + // Récords personales + highestDamageDealt Int @default(0) + longestWinStreak Int @default(0) + currentWinStreak Int @default(0) + mostCoinsAtOnce Int @default(0) + + user User @relation(fields: [userId], references: [id]) + guild Guild @relation(fields: [guildId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([userId, guildId]) + @@index([userId, guildId]) +} + +/** + * ----------------------------------------------------------------------------- + * Sistema de Rachas (Streaks) + * ----------------------------------------------------------------------------- + */ +model PlayerStreak { + id String @id @default(cuid()) + userId String + guildId String + + currentStreak Int @default(0) + longestStreak Int @default(0) + lastActiveDate DateTime @default(now()) + totalDaysActive Int @default(0) + + // Recompensas reclamadas por día + rewardsClaimed Json? // { day3: true, day7: true, etc } + + user User @relation(fields: [userId], references: [id]) + guild Guild @relation(fields: [guildId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([userId, guildId]) + @@index([userId, guildId]) +} + +/** + * ----------------------------------------------------------------------------- + * Log de Auditoría + * ----------------------------------------------------------------------------- + */ +model AuditLog { + id String @id @default(cuid()) + userId String + guildId String + action String // "buy", "craft", "trade", "equip", "mine", "fight", etc. + target String? // ID del item/mob/área afectado + details Json? // detalles adicionales + + user User @relation(fields: [userId], references: [id]) + guild Guild @relation(fields: [guildId], references: [id]) + + createdAt DateTime @default(now()) + + @@index([userId, guildId]) + @@index([action]) + @@index([createdAt]) +} diff --git a/src/commands/messages/game/tienda.ts b/src/commands/messages/game/tienda.ts new file mode 100644 index 0000000..0c99d62 --- /dev/null +++ b/src/commands/messages/game/tienda.ts @@ -0,0 +1,497 @@ +import type { CommandMessage } from '../../../core/types/commands'; +import type Amayo from '../../../core/client'; +import { + Message, + ButtonInteraction, + MessageComponentInteraction, + ComponentType, + ButtonStyle, + MessageFlags, + StringSelectMenuInteraction +} from 'discord.js'; +import { prisma } from '../../../core/database/prisma'; +import { getOrCreateWallet, buyFromOffer } from '../../../game/economy/service'; +import type { DisplayComponentContainer } from '../../../core/types/displayComponents'; +import type { ItemProps } from '../../../game/economy/types'; + +const ITEMS_PER_PAGE = 5; + +function parseItemProps(json: unknown): ItemProps { + if (!json || typeof json !== 'object') return {}; + return json as ItemProps; +} + +function formatPrice(price: any): string { + const parts: string[] = []; + if (price.coins) parts.push(`💰 ${price.coins}`); + if (price.items && price.items.length > 0) { + for (const item of price.items) { + parts.push(`📦 ${item.itemKey} x${item.qty}`); + } + } + return parts.join(' + ') || '¿Gratis?'; +} + +function getItemIcon(props: ItemProps, category?: string): string { + if (props.tool) { + const t = props.tool.type; + if (t === 'pickaxe') return '⛏️'; + if (t === 'rod') return '🎣'; + if (t === 'sword') return '🗡️'; + if (t === 'bow') return '🏹'; + if (t === 'halberd') return '⚔️'; + if (t === 'net') return '🕸️'; + return '🔧'; + } + if (props.damage && props.damage > 0) return '⚔️'; + if (props.defense && props.defense > 0) return '🛡️'; + if (props.food) return '🍖'; + if (props.chest) return '📦'; + if (category === 'consumables') return '🧪'; + if (category === 'materials') return '🔨'; + return '📦'; +} + +export const command: CommandMessage = { + name: 'tienda', + type: 'message', + aliases: ['shop', 'store'], + cooldown: 5, + description: 'Abre la tienda y navega por las ofertas disponibles con un panel interactivo.', + usage: 'tienda [categoria]', + run: async (message, args, _client: Amayo) => { + const userId = message.author.id; + const guildId = message.guild!.id; + + // Obtener wallet del usuario + const wallet = await getOrCreateWallet(userId, guildId); + + // Obtener todas las ofertas activas + const now = new Date(); + const offers = await prisma.shopOffer.findMany({ + where: { + guildId, + enabled: true, + OR: [ + { startAt: null, endAt: null }, + { startAt: { lte: now }, endAt: { gte: now } }, + { startAt: { lte: now }, endAt: null }, + { startAt: null, endAt: { gte: now } } + ] + }, + include: { item: true }, + orderBy: { createdAt: 'desc' } + }); + + if (offers.length === 0) { + await message.reply('🏪 La tienda está vacía. ¡Vuelve más tarde!'); + return; + } + + // Filtrar por categoría si se proporciona + const categoryFilter = args[0]?.trim().toLowerCase(); + const filteredOffers = categoryFilter + ? offers.filter(o => o.item.category?.toLowerCase().includes(categoryFilter)) + : offers; + + if (filteredOffers.length === 0) { + await message.reply(`🏪 No hay ofertas en la categoría "${categoryFilter}".`); + return; + } + + // Estado inicial + let currentPage = 1; + let selectedOfferId: string | null = null; + + const shopMessage = await message.reply({ + flags: MessageFlags.SuppressEmbeds | 32768, + components: await buildShopPanel(filteredOffers, currentPage, wallet.coins, selectedOfferId) + }); + + // Collector para interacciones + const collector = shopMessage.createMessageComponentCollector({ + time: 300000, // 5 minutos + filter: (i: MessageComponentInteraction) => i.user.id === message.author.id + }); + + collector.on('collect', async (interaction: MessageComponentInteraction) => { + try { + if (interaction.isButton()) { + await handleButtonInteraction( + interaction as ButtonInteraction, + filteredOffers, + currentPage, + selectedOfferId, + userId, + guildId, + shopMessage, + collector + ); + } else if (interaction.isStringSelectMenu()) { + await handleSelectInteraction( + interaction as StringSelectMenuInteraction, + filteredOffers, + currentPage, + userId, + guildId, + shopMessage + ); + } + + // Actualizar página y selección basado en customId + if (interaction.customId === 'shop_prev_page') { + currentPage = Math.max(1, currentPage - 1); + } else if (interaction.customId === 'shop_next_page') { + const totalPages = Math.ceil(filteredOffers.length / ITEMS_PER_PAGE); + currentPage = Math.min(totalPages, currentPage + 1); + } else if (interaction.customId === 'shop_select_item') { + // El select menu ya maneja la selección + } + } catch (error: any) { + console.error('Error handling shop interaction:', error); + if (!interaction.replied && !interaction.deferred) { + await interaction.reply({ + content: `❌ Error: ${error?.message ?? error}`, + flags: MessageFlags.Ephemeral + }); + } + } + }); + + collector.on('end', async (_, reason) => { + if (reason === 'time') { + try { + await shopMessage.edit({ + components: await buildExpiredPanel() + }); + } catch {} + } + }); + } +}; + +async function buildShopPanel( + offers: any[], + page: number, + userCoins: number, + selectedOfferId: string | null +): Promise { + const totalPages = Math.ceil(offers.length / ITEMS_PER_PAGE); + const safePage = Math.min(Math.max(1, page), totalPages); + const start = (safePage - 1) * ITEMS_PER_PAGE; + const pageOffers = offers.slice(start, start + ITEMS_PER_PAGE); + + // Encontrar la oferta seleccionada + const selectedOffer = selectedOfferId + ? offers.find(o => o.id === selectedOfferId) + : null; + + // Container principal + const container: DisplayComponentContainer = { + type: 17, + accent_color: 0xffa500, + components: [ + { + type: 10, + content: `🏪 **TIENDA** | Página ${safePage}/${totalPages}\n💰 Tus Monedas: **${userCoins}**` + }, + { + type: 14, + divider: true, + spacing: 2 + } + ] + }; + + // Si hay una oferta seleccionada, mostrar detalles + if (selectedOffer) { + const item = selectedOffer.item; + const props = parseItemProps(item.props); + const icon = getItemIcon(props, item.category); + const price = formatPrice(selectedOffer.price); + + // Stock info + let stockInfo = ''; + if (selectedOffer.stock != null) { + stockInfo = `\n📊 Stock: ${selectedOffer.stock}`; + } + if (selectedOffer.perUserLimit != null) { + const purchased = await prisma.shopPurchase.aggregate({ + where: { offerId: selectedOffer.id }, + _sum: { qty: true } + }); + const userPurchased = purchased._sum.qty ?? 0; + stockInfo += `\n👤 Límite por usuario: ${userPurchased}/${selectedOffer.perUserLimit}`; + } + + // Stats del item + let statsInfo = ''; + if (props.damage) statsInfo += `\n⚔️ Daño: +${props.damage}`; + if (props.defense) statsInfo += `\n🛡️ Defensa: +${props.defense}`; + if (props.maxHpBonus) statsInfo += `\n❤️ HP Bonus: +${props.maxHpBonus}`; + if (props.tool) statsInfo += `\n🔧 Herramienta: ${props.tool.type} T${props.tool.tier ?? 1}`; + if (props.food && props.food.healHp) statsInfo += `\n🍖 Cura: ${props.food.healHp} HP`; + + container.components.push({ + type: 10, + content: `${icon} **${item.name || item.key}**\n\n${item.description || 'Sin descripción'}${statsInfo}\n\n💰 Precio: ${price}${stockInfo}` + }); + + container.components.push({ + type: 14, + divider: true, + spacing: 1 + }); + } + + // Lista de ofertas en la página + container.components.push({ + type: 10, + content: selectedOffer ? '📋 **Otras Ofertas:**' : '📋 **Ofertas Disponibles:**' + }); + + for (const offer of pageOffers) { + const item = offer.item; + const props = parseItemProps(item.props); + const icon = getItemIcon(props, item.category); + const price = formatPrice(offer.price); + const isSelected = selectedOfferId === offer.id; + + const stockText = offer.stock != null ? ` (${offer.stock} disponibles)` : ''; + const selectedMark = isSelected ? ' ✓' : ''; + + container.components.push({ + type: 9, + components: [{ + type: 10, + content: `${icon} **${item.name || item.key}**${selectedMark}\n💰 ${price}${stockText}` + }], + accessory: { + type: 2, + style: isSelected ? ButtonStyle.Success : ButtonStyle.Primary, + label: isSelected ? 'Seleccionado' : 'Ver', + custom_id: `shop_view_${offer.id}` + } + }); + } + + // Botones de navegación y acciones + const actionRow1 = { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.Button, + style: ButtonStyle.Secondary, + label: '◀️ Anterior', + custom_id: 'shop_prev_page', + disabled: safePage <= 1 + }, + { + type: ComponentType.Button, + style: ButtonStyle.Secondary, + label: `Página ${safePage}/${totalPages}`, + custom_id: 'shop_current_page', + disabled: true + }, + { + type: ComponentType.Button, + style: ButtonStyle.Secondary, + label: 'Siguiente ▶️', + custom_id: 'shop_next_page', + disabled: safePage >= totalPages + } + ] + }; + + const actionRow2 = { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.Button, + style: ButtonStyle.Success, + label: '🛒 Comprar (x1)', + custom_id: 'shop_buy_1', + disabled: !selectedOfferId + }, + { + type: ComponentType.Button, + style: ButtonStyle.Success, + label: '🛒 Comprar (x5)', + custom_id: 'shop_buy_5', + disabled: !selectedOfferId + }, + { + type: ComponentType.Button, + style: ButtonStyle.Primary, + label: '🔄 Actualizar', + custom_id: 'shop_refresh' + }, + { + type: ComponentType.Button, + style: ButtonStyle.Danger, + label: '❌ Cerrar', + custom_id: 'shop_close' + } + ] + }; + + return [container, actionRow1, actionRow2]; +} + +async function handleButtonInteraction( + interaction: ButtonInteraction, + offers: any[], + currentPage: number, + selectedOfferId: string | null, + userId: string, + guildId: string, + shopMessage: Message, + collector: any +): Promise { + const customId = interaction.customId; + + // Ver detalles de un item + if (customId.startsWith('shop_view_')) { + const offerId = customId.replace('shop_view_', ''); + const wallet = await getOrCreateWallet(userId, guildId); + + await interaction.update({ + components: await buildShopPanel(offers, currentPage, wallet.coins, offerId) + }); + return; + } + + // Comprar + if (customId === 'shop_buy_1' || customId === 'shop_buy_5') { + if (!selectedOfferId) { + await interaction.reply({ + content: '❌ Primero selecciona un item.', + flags: MessageFlags.Ephemeral + }); + return; + } + + const qty = customId === 'shop_buy_1' ? 1 : 5; + + try { + await interaction.deferUpdate(); + const result = await buyFromOffer(userId, guildId, selectedOfferId, qty); + const wallet = await getOrCreateWallet(userId, guildId); + + await interaction.followUp({ + content: `✅ **Compra exitosa!**\n🛒 ${result.item.name || result.item.key} x${result.qty}\n💰 Te quedan: ${wallet.coins} monedas`, + flags: MessageFlags.Ephemeral + }); + + // Actualizar tienda + await shopMessage.edit({ + components: await buildShopPanel(offers, currentPage, wallet.coins, selectedOfferId) + }); + } catch (error: any) { + await interaction.followUp({ + content: `❌ No se pudo comprar: ${error?.message ?? error}`, + flags: MessageFlags.Ephemeral + }); + } + return; + } + + // Actualizar + if (customId === 'shop_refresh') { + const wallet = await getOrCreateWallet(userId, guildId); + await interaction.update({ + components: await buildShopPanel(offers, currentPage, wallet.coins, selectedOfferId) + }); + return; + } + + // Cerrar + if (customId === 'shop_close') { + await interaction.update({ + components: await buildClosedPanel() + }); + collector.stop(); + return; + } + + // Navegación de páginas (ya manejado en el collect) + if (customId === 'shop_prev_page' || customId === 'shop_next_page') { + const wallet = await getOrCreateWallet(userId, guildId); + let newPage = currentPage; + + if (customId === 'shop_prev_page') { + newPage = Math.max(1, currentPage - 1); + } else { + const totalPages = Math.ceil(offers.length / ITEMS_PER_PAGE); + newPage = Math.min(totalPages, currentPage + 1); + } + + await interaction.update({ + components: await buildShopPanel(offers, newPage, wallet.coins, selectedOfferId) + }); + return; + } +} + +async function handleSelectInteraction( + interaction: StringSelectMenuInteraction, + offers: any[], + currentPage: number, + userId: string, + guildId: string, + shopMessage: Message +): Promise { + // Si implementas un select menu, manejar aquí + await interaction.reply({ + content: 'Select menu no implementado aún', + flags: MessageFlags.Ephemeral + }); +} + +async function buildExpiredPanel(): Promise { + const container: DisplayComponentContainer = { + type: 17, + accent_color: 0x36393f, + components: [ + { + type: 10, + content: '⏰ **Tienda Expirada**' + }, + { + type: 14, + divider: true, + spacing: 1 + }, + { + type: 10, + content: 'La sesión de tienda ha expirado.\nUsa `!tienda` nuevamente para ver las ofertas.' + } + ] + }; + + return [container]; +} + +async function buildClosedPanel(): Promise { + const container: DisplayComponentContainer = { + type: 17, + accent_color: 0x36393f, + components: [ + { + type: 10, + content: '✅ **Tienda Cerrada**' + }, + { + type: 14, + divider: true, + spacing: 1 + }, + { + type: 10, + content: '¡Gracias por visitar la tienda!\nVuelve pronto. 🛒' + } + ] + }; + + return [container]; +}