feat(economy): enhance minigame commands to track stats, quest progress, and achievements

This commit is contained in:
2025-10-05 05:35:32 -05:00
parent b10cc89583
commit 0990533f6e
15 changed files with 954 additions and 30 deletions

185
IMPLEMENTATION_SUMMARY.md Normal file
View File

@@ -0,0 +1,185 @@
# 🎉 Resumen de Implementación - Sistema de Engagement
## ✅ Lo que se ha implementado
### 📊 Servicios Creados
#### 1. **Sistema de Estadísticas** (`src/game/stats/`)
-`service.ts` - Servicio completo para tracking de estadísticas
-`types.ts` - Tipos TypeScript para stats
- **Funcionalidades:**
- Tracking de minas, pesca, combates, granja
- Estadísticas de combate (daño, mobs derrotados)
- Estadísticas económicas (monedas ganadas/gastadas, items crafteados)
- Récords personales
- Leaderboards por categoría
#### 2. **Sistema de Recompensas** (`src/game/rewards/`)
-`service.ts` - Sistema centralizado de recompensas
- **Funcionalidades:**
- Dar monedas, items, XP y títulos
- Validación de recompensas
- Logging automático de auditoría
#### 3. **Sistema de Logros** (`src/game/achievements/`)
-`service.ts` - Sistema completo de achievements
-`seed.ts` - 17 logros base pre-configurados
- **Funcionalidades:**
- Verificación automática de logros por triggers
- Tracking de progreso
- Logros ocultos
- Sistema de puntos
- Barras de progreso visuales
#### 4. **Sistema de Rachas** (`src/game/streaks/`)
-`service.ts` - Sistema de rachas diarias
- **Funcionalidades:**
- Tracking de días consecutivos
- Recompensas escaladas por día
- Hitos especiales (día 7, 14, 30, etc.)
- Detección automática de expiración
#### 5. **Sistema de Misiones** (`src/game/quests/`)
-`service.ts` - Sistema completo de quests
- **Funcionalidades:**
- Misiones diarias, semanales, permanentes y de evento
- Actualización automática de progreso
- Generación automática de misiones diarias
- Sistema de reclamación de recompensas
- Limpieza automática de misiones expiradas
### 🎮 Comandos Creados
#### 1. **!stats** (`src/commands/messages/game/stats.ts`)
- Ver estadísticas detalladas propias o de otro jugador
- Categorías: Actividades, Combate, Economía, Items, Récords
- Embed visual con avatar del jugador
#### 2. **!racha** (`src/commands/messages/game/racha.ts`)
- Ver y reclamar racha diaria
- Muestra racha actual, mejor racha y días activos
- Recompensas automáticas al reclamar
- Indicador de próximos hitos
#### 3. **!cooldowns** (`src/commands/messages/game/cooldowns.ts`)
- Ver todos los cooldowns activos
- Formato amigable con emojis
- Tiempo restante formateado (horas, minutos, segundos)
#### 4. **!logros** (`src/commands/messages/game/logros.ts`)
- Ver logros desbloqueados y en progreso
- Barras de progreso visuales
- Estadísticas de logros (total, puntos)
- Categorización por tipo
#### 5. **!misiones** (`src/commands/messages/game/misiones.ts`)
- Ver misiones disponibles por tipo
- Progreso visual de cada misión
- Indicador de misiones listas para reclamar
- Recompensas mostradas
#### 6. **!mision-reclamar** (`src/commands/messages/game/misionReclamar.ts`)
- Reclamar recompensas de misiones completadas
- Validación automática
- Confirmación visual de recompensas recibidas
### 🔗 Integraciones
#### Comandos Existentes Mejorados:
-**!mina** - Ahora trackea stats, actualiza misiones y verifica logros
-**!pescar** - Ahora trackea stats, actualiza misiones y verifica logros
-**!pelear** - Ahora trackea stats, actualiza misiones y verifica logros
### 📦 Logros Pre-configurados
**Minería (4 logros):**
- ⛏️ Primera Mina (1 vez)
- ⛏️ Minero Novato (10 veces)
- ⛏️ Minero Experto (50 veces)
- ⛏️ Maestro Minero (100 veces)
**Pesca (3 logros):**
- 🎣 Primera Pesca (1 vez)
- 🎣 Pescador Novato (10 veces)
- 🎣 Pescador Experto (50 veces)
**Combate (4 logros):**
- ⚔️ Primera Pelea (1 vez)
- ⚔️ Guerrero Novato (10 veces)
- 👾 Cazador de Monstruos (50 mobs)
- 👾 Asesino de Monstruos (200 mobs)
**Economía (3 logros):**
- 💰 Primeras Monedas (1,000 monedas)
- 💰 Acaudalado (10,000 monedas)
- 💰 Millonario (100,000 monedas)
**Crafteo (3 logros):**
- 🛠️ Primer Crafteo (1 item)
- 🛠️ Artesano Experto (50 items)
- 🛠️ Maestro Artesano (200 items)
## 🚀 Cómo Usar
### Inicializar Logros Base
```bash
npx ts-node src/game/achievements/seed.ts
```
### Comandos para Usuarios
```
!stats [@usuario] - Ver estadísticas
!racha - Ver/reclamar racha diaria
!cooldowns - Ver cooldowns activos
!logros [@usuario] - Ver logros
!misiones - Ver misiones disponibles
!mision-reclamar <número> - Reclamar recompensa de misión
```
### Sistema Automático
- Las estadísticas se actualizan automáticamente al usar comandos
- Los logros se verifican después de cada acción
- Las misiones se actualizan en tiempo real
- Las rachas se actualizan al usar !racha
## 🎯 Próximos Pasos Sugeridos
### Fase 2 - Mejoras Adicionales:
1. ⬜ Crear más logros (objetivo: 50+)
2. ⬜ Implementar generación automática de misiones diarias (cron job)
3. ⬜ Crear comando `!ranking-stats` para leaderboards
4. ⬜ Añadir notificaciones por DM para logros importantes
5. ⬜ Implementar sistema de títulos desbloqueables
6. ⬜ Crear misiones semanales
7. ⬜ Sistema de eventos temporales
### Fase 3 - Social:
1. ⬜ Sistema de clanes/guilds
2. ⬜ Trading entre jugadores
3. ⬜ Comparar stats con otros jugadores
4. ⬜ Logros cooperativos
## 📝 Notas Técnicas
- ✅ Todo el código está completamente tipado con TypeScript
- ✅ Los modelos de Prisma ya existen (Achievement, Quest, PlayerStats, etc.)
- ✅ Sistema de recompensas centralizado y reutilizable
- ✅ Logging automático de auditoría
- ✅ Manejo de errores robusto
- ✅ Compatible con sistema de guildId global/por servidor
## 🐛 Testing Recomendado
1. Probar cada comando nuevo individualmente
2. Verificar que los stats se incrementan correctamente
3. Confirmar que los logros se desbloquean al cumplir requisitos
4. Verificar el sistema de rachas por múltiples días
5. Probar reclamación de misiones
6. Verificar cooldowns
## 🔧 Mantenimiento
- Los logros se pueden añadir fácilmente en `seed.ts`
- Las misiones se pueden configurar con templates en `quests/service.ts`
- Las recompensas de rachas se pueden ajustar en `streaks/service.ts`
- Los triggers de logros son extensibles (añadir más tipos en achievements/service.ts)

View File

@@ -0,0 +1,109 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { prisma } from '../../../core/database/prisma';
import { EmbedBuilder } from 'discord.js';
export const command: CommandMessage = {
name: 'cooldowns',
type: 'message',
aliases: ['cds', 'tiempos', 'cd'],
cooldown: 3,
description: 'Ver todos tus cooldowns activos',
usage: 'cooldowns',
run: async (message, args, client: Amayo) => {
try {
const userId = message.author.id;
const guildId = message.guild!.id;
// Obtener todos los cooldowns activos
const cooldowns = await prisma.actionCooldown.findMany({
where: {
userId,
guildId,
until: { gt: new Date() }
},
orderBy: { until: 'asc' }
});
if (cooldowns.length === 0) {
await message.reply('✅ No tienes cooldowns activos. ¡Puedes realizar cualquier acción!');
return;
}
const embed = new EmbedBuilder()
.setColor(0xFF6B6B)
.setTitle('⏰ Cooldowns Activos')
.setDescription(`${message.author.username}, estos son tus cooldowns:`)
.setThumbnail(message.author.displayAvatarURL({ size: 128 }));
// Emojis por tipo de acción
const actionEmojis: Record<string, string> = {
'mine': '⛏️',
'fish': '🎣',
'fight': '⚔️',
'farm': '🌾',
'craft': '🛠️',
'smelt': '🔥',
'shop': '🛒',
'daily': '🎁',
'consume': '🍖'
};
// Traducción de acciones
const actionNames: Record<string, string> = {
'mine': 'Minar',
'fish': 'Pescar',
'fight': 'Pelear',
'farm': 'Granja',
'craft': 'Craftear',
'smelt': 'Fundir',
'shop': 'Tienda',
'daily': 'Diario',
'consume': 'Consumir'
};
let cooldownText = '';
const now = Date.now();
for (const cd of cooldowns) {
const remainingMs = cd.until.getTime() - now;
const remainingSec = Math.ceil(remainingMs / 1000);
// Formatear tiempo
let timeStr = '';
if (remainingSec >= 3600) {
const hours = Math.floor(remainingSec / 3600);
const mins = Math.floor((remainingSec % 3600) / 60);
timeStr = `${hours}h ${mins}m`;
} else if (remainingSec >= 60) {
const mins = Math.floor(remainingSec / 60);
const secs = remainingSec % 60;
timeStr = `${mins}m ${secs}s`;
} else {
timeStr = `${remainingSec}s`;
}
// Buscar emoji y nombre
const action = cd.key.split(':')[0];
const emoji = actionEmojis[action] || '⏱️';
const actionName = actionNames[action] || cd.key;
cooldownText += `${emoji} **${actionName}**: ${timeStr}\n`;
}
embed.addFields({
name: `📋 Cooldowns (${cooldowns.length})`,
value: cooldownText,
inline: false
});
embed.setFooter({ text: 'Los cooldowns se actualizan en tiempo real' });
embed.setTimestamp();
await message.reply({ embeds: [embed] });
} catch (error) {
console.error('Error en comando cooldowns:', error);
await message.reply('❌ Error al obtener los cooldowns.');
}
}
};

View File

@@ -0,0 +1,117 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { getPlayerAchievements, getAchievementStats, createProgressBar } from '../../../game/achievements/service';
import { EmbedBuilder } from 'discord.js';
export const command: CommandMessage = {
name: 'logros',
type: 'message',
aliases: ['achievements', 'logro', 'achievement'],
cooldown: 5,
description: 'Ver tus logros desbloqueados y progreso',
usage: 'logros [@usuario]',
run: async (message, args, client: Amayo) => {
try {
const guildId = message.guild!.id;
const targetUser = message.mentions.users.first() || message.author;
const userId = targetUser.id;
// Obtener logros del jugador
const { unlocked, inProgress } = await getPlayerAchievements(userId, guildId);
const achievementStats = await getAchievementStats(userId, guildId);
const embed = new EmbedBuilder()
.setColor(0xFFD700)
.setTitle(`🏆 Logros de ${targetUser.username}`)
.setThumbnail(targetUser.displayAvatarURL({ size: 128 }))
.setDescription(
`**${achievementStats.unlocked}/${achievementStats.total}** logros desbloqueados ` +
`(${achievementStats.percentage}%)\n` +
`⭐ **${achievementStats.points}** puntos totales`
);
// Logros desbloqueados recientes (últimos 5)
if (unlocked.length > 0) {
const recentUnlocked = unlocked.slice(0, 5);
let unlockedText = '';
for (const pa of recentUnlocked) {
const icon = pa.achievement.icon || '🏆';
const points = pa.achievement.points || 10;
unlockedText += `${icon} **${pa.achievement.name}** (+${points} pts)\n`;
unlockedText += `${pa.achievement.description}\n`;
}
embed.addFields({
name: `✅ Desbloqueados Recientes (${unlocked.length})`,
value: unlockedText || 'Ninguno aún',
inline: false
});
}
// Logros en progreso (top 5)
if (inProgress.length > 0) {
const topInProgress = inProgress.slice(0, 5);
let progressText = '';
for (const pa of topInProgress) {
const icon = pa.achievement.icon || '🔒';
const req = pa.achievement.requirements as any;
const progress = pa.progress;
const required = req.value;
const bar = createProgressBar(progress, required, 8);
progressText += `${icon} **${pa.achievement.name}**\n`;
progressText += `${bar} (${progress}/${required})\n`;
}
embed.addFields({
name: `📈 En Progreso (${inProgress.length})`,
value: progressText,
inline: false
});
}
// Categorías
const categories = ['mining', 'fishing', 'combat', 'economy', 'exploration'];
const categoryEmojis: Record<string, string> = {
mining: '⛏️',
fishing: '🎣',
combat: '⚔️',
economy: '💰',
exploration: '🗺️'
};
let categoryText = '';
for (const cat of categories) {
const count = unlocked.filter(pa => pa.achievement.category === cat).length;
if (count > 0) {
categoryText += `${categoryEmojis[cat]} ${count} `;
}
}
if (categoryText) {
embed.addFields({
name: '📊 Por Categoría',
value: categoryText,
inline: false
});
}
if (unlocked.length === 0 && inProgress.length === 0) {
embed.setDescription(
'Aún no has desbloqueado ningún logro.\n' +
'¡Empieza a jugar para obtener logros y puntos!'
);
}
embed.setFooter({ text: 'Los logros se desbloquean automáticamente al cumplir requisitos' });
embed.setTimestamp();
await message.reply({ embeds: [embed] });
} catch (error) {
console.error('Error en comando logros:', error);
await message.reply('❌ Error al obtener los logros.');
}
}
};

View File

@@ -2,6 +2,9 @@ import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client'; import type Amayo from '../../../core/client';
import { runMinigame } from '../../../game/minigames/service'; import { runMinigame } from '../../../game/minigames/service';
import { resolveArea, getDefaultLevel, findBestToolKey } from './_helpers'; import { resolveArea, getDefaultLevel, findBestToolKey } from './_helpers';
import { updateStats } from '../../../game/stats/service';
import { updateQuestProgress } from '../../../game/quests/service';
import { checkAchievements } from '../../../game/achievements/service';
export const command: CommandMessage = { export const command: CommandMessage = {
name: 'mina', name: 'mina',
@@ -26,13 +29,34 @@ export const command: CommandMessage = {
try { try {
const result = await runMinigame(userId, guildId, areaKey, level, { toolKey: toolKey ?? undefined }); const result = await runMinigame(userId, guildId, areaKey, level, { toolKey: toolKey ?? undefined });
// Actualizar stats
await updateStats(userId, guildId, { minesCompleted: 1 });
// Actualizar progreso de misiones
await updateQuestProgress(userId, guildId, 'mine_count', 1);
// Verificar logros
const newAchievements = await checkAchievements(userId, guildId, 'mine_count');
const rewards = result.rewards.map(r => r.type === 'coins' ? `🪙 +${r.amount}` : `📦 ${r.itemKey} x${r.qty}`).join(' · ') || '—'; const rewards = result.rewards.map(r => r.type === 'coins' ? `🪙 +${r.amount}` : `📦 ${r.itemKey} x${r.qty}`).join(' · ') || '—';
const mobs = result.mobs.length ? result.mobs.join(', ') : '—'; const mobs = result.mobs.length ? result.mobs.join(', ') : '—';
const toolInfo = result.tool?.key ? `🔧 ${result.tool.key}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta} dur.)`}` : '—'; const toolInfo = result.tool?.key ? `🔧 ${result.tool.key}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta} dur.)`}` : '—';
await message.reply(`⛏️ Mina (nivel ${level})
let response = `⛏️ Mina (nivel ${level})
Recompensas: ${rewards} Recompensas: ${rewards}
Mobs: ${mobs} Mobs: ${mobs}
Herramienta: ${toolInfo}`); Herramienta: ${toolInfo}`;
// Notificar logros desbloqueados
if (newAchievements.length > 0) {
response += `\n\n🏆 ¡Logro desbloqueado!`;
for (const ach of newAchievements) {
response += `\n✨ **${ach.name}** - ${ach.description}`;
}
}
await message.reply(response);
} catch (e: any) { } catch (e: any) {
await message.reply(`❌ No se pudo minar: ${e?.message ?? e}`); await message.reply(`❌ No se pudo minar: ${e?.message ?? e}`);
} }

View File

@@ -0,0 +1,73 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { claimQuestReward, getPlayerQuests } from '../../../game/quests/service';
import { EmbedBuilder } from 'discord.js';
export const command: CommandMessage = {
name: 'mision-reclamar',
type: 'message',
aliases: ['claim-quest', 'reclamar-mision'],
cooldown: 3,
description: 'Reclamar recompensa de misión completada',
usage: 'mision-reclamar <numero>',
run: async (message, args, client: Amayo) => {
try {
const userId = message.author.id;
const guildId = message.guild!.id;
if (!args[0]) {
await message.reply(`❌ Uso: \`!mision-reclamar <numero>\`\nEjemplo: \`!mision-reclamar 1\``);
return;
}
// Obtener misiones completadas
const quests = await getPlayerQuests(userId, guildId);
const allQuests = [...quests.daily, ...quests.weekly, ...quests.permanent, ...quests.event];
const claimable = allQuests.filter(q => q.canClaim);
if (claimable.length === 0) {
await message.reply('❌ No tienes misiones listas para reclamar. Completa misiones primero usando los comandos del bot.');
return;
}
const index = parseInt(args[0]) - 1;
if (isNaN(index) || index < 0 || index >= claimable.length) {
await message.reply(`❌ Número de misión inválido. Elige un número entre 1 y ${claimable.length}.`);
return;
}
const selected = claimable[index];
// Reclamar recompensa
const { quest, rewards } = await claimQuestReward(userId, guildId, selected.quest.id);
const embed = new EmbedBuilder()
.setColor(0x00FF00)
.setTitle('🎉 ¡Misión Completada!')
.setDescription(`Has reclamado las recompensas de **${quest.name}**`)
.setThumbnail(message.author.displayAvatarURL({ size: 128 }));
// Mostrar recompensas
if (rewards.length > 0) {
embed.addFields({
name: '🎁 Recompensas Recibidas',
value: rewards.join('\n'),
inline: false
});
}
// Info de la misión
embed.addFields(
{ name: '📜 Misión', value: quest.description, inline: false }
);
embed.setFooter({ text: `Usa !misiones para ver más misiones` });
embed.setTimestamp();
await message.reply({ embeds: [embed] });
} catch (error: any) {
console.error('Error en comando mision-reclamar:', error);
await message.reply(`${error.message || 'Error al reclamar la misión.'}`);
}
}
};

View File

@@ -0,0 +1,157 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { getPlayerQuests, claimQuestReward } from '../../../game/quests/service';
import { createProgressBar } from '../../../game/achievements/service';
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
export const command: CommandMessage = {
name: 'misiones',
type: 'message',
aliases: ['quests', 'mision', 'quest'],
cooldown: 5,
description: 'Ver misiones disponibles y tu progreso',
usage: 'misiones [categoria]',
run: async (message, args, client: Amayo) => {
try {
const userId = message.author.id;
const guildId = message.guild!.id;
// Obtener misiones con progreso
const quests = await getPlayerQuests(userId, guildId);
const embed = new EmbedBuilder()
.setColor(0x5865F2)
.setTitle('📜 Misiones Disponibles')
.setDescription(`${message.author.username}, aquí están tus misiones:`)
.setThumbnail(message.author.displayAvatarURL({ size: 128 }));
// Emojis por categoría
const categoryEmojis: Record<string, string> = {
mining: '⛏️',
fishing: '🎣',
combat: '⚔️',
economy: '💰',
exploration: '🗺️',
crafting: '🛠️'
};
// Función para formatear una lista de misiones
const formatQuests = (questList: any[], type: string) => {
if (questList.length === 0) return null;
let text = '';
for (const { quest, progress, canClaim, percentage } of questList) {
const icon = categoryEmojis[quest.category] || '📋';
const req = quest.requirements as any;
const currentProgress = progress?.progress || 0;
const required = req.count;
// Estado
let status = '';
if (canClaim) {
status = '✅ ¡Listo para reclamar!';
} else if (progress?.completed) {
status = '🎁 Completada';
} else {
const bar = createProgressBar(currentProgress, required, 8);
status = `${bar}`;
}
text += `${icon} **${quest.name}**\n`;
text += `${quest.description}\n`;
text += `${status}\n`;
// Recompensas
const rewards = quest.rewards as any;
let rewardStr = '';
if (rewards.coins) rewardStr += `💰 ${rewards.coins} `;
if (rewards.items && rewards.items.length > 0) {
rewardStr += `📦 ${rewards.items.length} items `;
}
if (rewardStr) {
text += `└ Recompensa: ${rewardStr}\n`;
}
text += '\n';
}
return text;
};
// Misiones diarias
if (quests.daily.length > 0) {
const dailyText = formatQuests(quests.daily, 'daily');
if (dailyText) {
embed.addFields({
name: '📅 Misiones Diarias',
value: dailyText,
inline: false
});
}
}
// Misiones semanales
if (quests.weekly.length > 0) {
const weeklyText = formatQuests(quests.weekly, 'weekly');
if (weeklyText) {
embed.addFields({
name: '📆 Misiones Semanales',
value: weeklyText,
inline: false
});
}
}
// Misiones permanentes
if (quests.permanent.length > 0) {
const permanentText = formatQuests(quests.permanent.slice(0, 3), 'permanent');
if (permanentText) {
embed.addFields({
name: '♾️ Misiones Permanentes',
value: permanentText,
inline: false
});
}
}
// Misiones de evento
if (quests.event.length > 0) {
const eventText = formatQuests(quests.event, 'event');
if (eventText) {
embed.addFields({
name: '🎉 Misiones de Evento',
value: eventText,
inline: false
});
}
}
// Verificar si hay misiones para reclamar
const canClaim = [...quests.daily, ...quests.weekly, ...quests.permanent, ...quests.event]
.filter(q => q.canClaim);
if (canClaim.length > 0) {
embed.addFields({
name: '🎁 ¡Misiones Listas!',
value: `Tienes **${canClaim.length}** misiones listas para reclamar.\nUsa \`!mision-reclamar <id>\` para reclamar recompensas.`,
inline: false
});
}
if (quests.daily.length === 0 && quests.weekly.length === 0 && quests.permanent.length === 0) {
embed.setDescription(
'No hay misiones disponibles en este momento.\n' +
'Las misiones diarias se generan automáticamente cada día.'
);
}
embed.setFooter({ text: 'Completa misiones para ganar recompensas' });
embed.setTimestamp();
await message.reply({ embeds: [embed] });
} catch (error) {
console.error('Error en comando misiones:', error);
await message.reply('❌ Error al obtener las misiones.');
}
}
};

View File

@@ -2,6 +2,9 @@ import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client'; import type Amayo from '../../../core/client';
import { runMinigame } from '../../../game/minigames/service'; import { runMinigame } from '../../../game/minigames/service';
import { resolveArea, getDefaultLevel, findBestToolKey } from './_helpers'; import { resolveArea, getDefaultLevel, findBestToolKey } from './_helpers';
import { updateStats } from '../../../game/stats/service';
import { updateQuestProgress } from '../../../game/quests/service';
import { checkAchievements } from '../../../game/achievements/service';
export const command: CommandMessage = { export const command: CommandMessage = {
name: 'pelear', name: 'pelear',
@@ -26,13 +29,37 @@ export const command: CommandMessage = {
try { try {
const result = await runMinigame(userId, guildId, areaKey, level, { toolKey: toolKey ?? undefined }); const result = await runMinigame(userId, guildId, areaKey, level, { toolKey: toolKey ?? undefined });
// Actualizar stats y misiones
await updateStats(userId, guildId, { fightsCompleted: 1 });
await updateQuestProgress(userId, guildId, 'fight_count', 1);
// Contar mobs derrotados
const mobsCount = result.mobs.length;
if (mobsCount > 0) {
await updateStats(userId, guildId, { mobsDefeated: mobsCount });
await updateQuestProgress(userId, guildId, 'mob_defeat_count', mobsCount);
}
const newAchievements = await checkAchievements(userId, guildId, 'fight_count');
const rewards = result.rewards.map(r => r.type === 'coins' ? `🪙 +${r.amount}` : `🎁 ${r.itemKey} x${r.qty}`).join(' · ') || '—'; const rewards = result.rewards.map(r => r.type === 'coins' ? `🪙 +${r.amount}` : `🎁 ${r.itemKey} x${r.qty}`).join(' · ') || '—';
const mobs = result.mobs.length ? result.mobs.join(', ') : '—'; const mobs = result.mobs.length ? result.mobs.join(', ') : '—';
const toolInfo = result.tool?.key ? `🗡️ ${result.tool.key}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta} dur.)`}` : '—'; const toolInfo = result.tool?.key ? `🗡️ ${result.tool.key}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta} dur.)`}` : '—';
await message.reply(`⚔️ Arena (nivel ${level})
let response = `⚔️ Arena (nivel ${level})
Recompensas: ${rewards} Recompensas: ${rewards}
Enemigos: ${mobs} Enemigos: ${mobs}
Arma: ${toolInfo}`); Arma: ${toolInfo}`;
if (newAchievements.length > 0) {
response += `\n\n🏆 ¡Logro desbloqueado!`;
for (const ach of newAchievements) {
response += `\n✨ **${ach.name}** - ${ach.description}`;
}
}
await message.reply(response);
} catch (e: any) { } catch (e: any) {
await message.reply(`❌ No se pudo pelear: ${e?.message ?? e}`); await message.reply(`❌ No se pudo pelear: ${e?.message ?? e}`);
} }

View File

@@ -2,6 +2,9 @@ import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client'; import type Amayo from '../../../core/client';
import { runMinigame } from '../../../game/minigames/service'; import { runMinigame } from '../../../game/minigames/service';
import { resolveArea, getDefaultLevel, findBestToolKey } from './_helpers'; import { resolveArea, getDefaultLevel, findBestToolKey } from './_helpers';
import { updateStats } from '../../../game/stats/service';
import { updateQuestProgress } from '../../../game/quests/service';
import { checkAchievements } from '../../../game/achievements/service';
export const command: CommandMessage = { export const command: CommandMessage = {
name: 'pescar', name: 'pescar',
@@ -26,13 +29,29 @@ export const command: CommandMessage = {
try { try {
const result = await runMinigame(userId, guildId, areaKey, level, { toolKey: toolKey ?? undefined }); const result = await runMinigame(userId, guildId, areaKey, level, { toolKey: toolKey ?? undefined });
// Actualizar stats y misiones
await updateStats(userId, guildId, { fishingCompleted: 1 });
await updateQuestProgress(userId, guildId, 'fish_count', 1);
const newAchievements = await checkAchievements(userId, guildId, 'fish_count');
const rewards = result.rewards.map(r => r.type === 'coins' ? `🪙 +${r.amount}` : `🐟 ${r.itemKey} x${r.qty}`).join(' · ') || '—'; const rewards = result.rewards.map(r => r.type === 'coins' ? `🪙 +${r.amount}` : `🐟 ${r.itemKey} x${r.qty}`).join(' · ') || '—';
const mobs = result.mobs.length ? result.mobs.join(', ') : '—'; const mobs = result.mobs.length ? result.mobs.join(', ') : '—';
const toolInfo = result.tool?.key ? `🎣 ${result.tool.key}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta} dur.)`}` : '—'; const toolInfo = result.tool?.key ? `🎣 ${result.tool.key}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta} dur.)`}` : '—';
await message.reply(`🎣 Pesca (nivel ${level})
let response = `🎣 Pesca (nivel ${level})
Recompensas: ${rewards} Recompensas: ${rewards}
Mobs: ${mobs} Mobs: ${mobs}
Herramienta: ${toolInfo}`); Herramienta: ${toolInfo}`;
if (newAchievements.length > 0) {
response += `\n\n🏆 ¡Logro desbloqueado!`;
for (const ach of newAchievements) {
response += `\n✨ **${ach.name}** - ${ach.description}`;
}
}
await message.reply(response);
} catch (e: any) { } catch (e: any) {
await message.reply(`❌ No se pudo pescar: ${e?.message ?? e}`); await message.reply(`❌ No se pudo pescar: ${e?.message ?? e}`);
} }

View File

@@ -66,7 +66,7 @@ export const command: CommandMessage = {
embed.addFields({ name: '🏆 Récords', value: recordsText || 'Sin datos', inline: true }); embed.addFields({ name: '🏆 Récords', value: recordsText || 'Sin datos', inline: true });
} }
embed.setFooter({ text: `Usa ${client.prefix}ranking-stats para ver el ranking global` }); embed.setFooter({ text: 'Usa !ranking-stats para ver el ranking global' });
await message.reply({ embeds: [embed] }); await message.reply({ embeds: [embed] });
} catch (error) { } catch (error) {

View File

@@ -0,0 +1,221 @@
import { prisma } from '../../core/database/prisma';
import logger from '../../core/lib/logger';
/**
* Seed de logros base
*/
export async function seedAchievements(guildId: string | null = null) {
const achievements = [
// Minería
{
key: 'first_mine',
name: '⛏️ Primera Mina',
description: 'Mina por primera vez',
category: 'mining',
requirements: { type: 'mine_count', value: 1 },
rewards: { coins: 100 },
hidden: false,
points: 10
},
{
key: 'miner_novice',
name: '⛏️ Minero Novato',
description: 'Mina 10 veces',
category: 'mining',
requirements: { type: 'mine_count', value: 10 },
rewards: { coins: 500 },
hidden: false,
points: 20
},
{
key: 'miner_expert',
name: '⛏️ Minero Experto',
description: 'Mina 50 veces',
category: 'mining',
requirements: { type: 'mine_count', value: 50 },
rewards: { coins: 2500 },
hidden: false,
points: 50
},
{
key: 'miner_master',
name: '⛏️ Maestro Minero',
description: 'Mina 100 veces',
category: 'mining',
requirements: { type: 'mine_count', value: 100 },
rewards: { coins: 10000 },
hidden: false,
points: 100
},
// Pesca
{
key: 'first_fish',
name: '🎣 Primera Pesca',
description: 'Pesca por primera vez',
category: 'fishing',
requirements: { type: 'fish_count', value: 1 },
rewards: { coins: 100 },
hidden: false,
points: 10
},
{
key: 'fisher_novice',
name: '🎣 Pescador Novato',
description: 'Pesca 10 veces',
category: 'fishing',
requirements: { type: 'fish_count', value: 10 },
rewards: { coins: 500 },
hidden: false,
points: 20
},
{
key: 'fisher_expert',
name: '🎣 Pescador Experto',
description: 'Pesca 50 veces',
category: 'fishing',
requirements: { type: 'fish_count', value: 50 },
rewards: { coins: 2500 },
hidden: false,
points: 50
},
// Combate
{
key: 'first_fight',
name: '⚔️ Primera Pelea',
description: 'Pelea por primera vez',
category: 'combat',
requirements: { type: 'fight_count', value: 1 },
rewards: { coins: 150 },
hidden: false,
points: 10
},
{
key: 'warrior_novice',
name: '⚔️ Guerrero Novato',
description: 'Pelea 10 veces',
category: 'combat',
requirements: { type: 'fight_count', value: 10 },
rewards: { coins: 750 },
hidden: false,
points: 20
},
{
key: 'mob_hunter',
name: '👾 Cazador de Monstruos',
description: 'Derrota 50 mobs',
category: 'combat',
requirements: { type: 'mob_defeat_count', value: 50 },
rewards: { coins: 3000 },
hidden: false,
points: 50
},
{
key: 'mob_slayer',
name: '👾 Asesino de Monstruos',
description: 'Derrota 200 mobs',
category: 'combat',
requirements: { type: 'mob_defeat_count', value: 200 },
rewards: { coins: 15000 },
hidden: false,
points: 100
},
// Economía
{
key: 'first_coins',
name: '💰 Primeras Monedas',
description: 'Gana 1,000 monedas en total',
category: 'economy',
requirements: { type: 'coins_earned', value: 1000 },
rewards: { coins: 200 },
hidden: false,
points: 10
},
{
key: 'wealthy',
name: '💰 Acaudalado',
description: 'Gana 10,000 monedas en total',
category: 'economy',
requirements: { type: 'coins_earned', value: 10000 },
rewards: { coins: 2000 },
hidden: false,
points: 30
},
{
key: 'millionaire',
name: '💰 Millonario',
description: 'Gana 100,000 monedas en total',
category: 'economy',
requirements: { type: 'coins_earned', value: 100000 },
rewards: { coins: 25000 },
hidden: false,
points: 100
},
// Crafteo
{
key: 'first_craft',
name: '🛠️ Primer Crafteo',
description: 'Craftea tu primer item',
category: 'crafting',
requirements: { type: 'craft_count', value: 1 },
rewards: { coins: 100 },
hidden: false,
points: 10
},
{
key: 'crafter_expert',
name: '🛠️ Artesano Experto',
description: 'Craftea 50 items',
category: 'crafting',
requirements: { type: 'craft_count', value: 50 },
rewards: { coins: 5000 },
hidden: false,
points: 50
},
{
key: 'master_crafter',
name: '🛠️ Maestro Artesano',
description: 'Craftea 200 items',
category: 'crafting',
requirements: { type: 'craft_count', value: 200 },
rewards: { coins: 20000 },
hidden: false,
points: 100
}
];
let created = 0;
for (const ach of achievements) {
const existing = await prisma.achievement.findUnique({
where: { guildId_key: { guildId: guildId || '', key: ach.key } }
});
if (!existing) {
await prisma.achievement.create({
data: { ...ach, guildId: guildId || undefined }
});
created++;
}
}
console.log(`Seeded ${created} achievements for guild ${guildId || 'global'}`);
return created;
}
/**
* Ejecutar si es llamado directamente
*/
if (require.main === module) {
seedAchievements(null)
.then(count => {
console.log(`${count} achievements seeded`);
process.exit(0);
})
.catch(error => {
console.error('❌ Error seeding achievements:', error);
process.exit(1);
});
}

View File

@@ -26,7 +26,7 @@ export async function checkAchievements(
} }
}); });
const newUnlocks = []; const newUnlocks: any[] = [];
const stats = await getOrCreatePlayerStats(userId, guildId); const stats = await getOrCreatePlayerStats(userId, guildId);
for (const achievement of achievements) { for (const achievement of achievements) {
@@ -118,7 +118,7 @@ export async function checkAchievements(
return newUnlocks; return newUnlocks;
} catch (error) { } catch (error) {
logger.error(`Error checking achievements for ${userId}:`, error); console.error(`Error checking achievements for ${userId}:`, error);
return []; return [];
} }
} }

View File

@@ -16,15 +16,11 @@ export async function updateQuestProgress(
const quests = await prisma.quest.findMany({ const quests = await prisma.quest.findMany({
where: { where: {
OR: [{ guildId }, { guildId: null }], OR: [{ guildId }, { guildId: null }],
active: true, active: true
OR: [
{ endAt: null },
{ endAt: { gte: new Date() } }
]
} }
}); });
const updates = []; const updates: any[] = [];
for (const quest of quests) { for (const quest of quests) {
const req = quest.requirements as any; const req = quest.requirements as any;
@@ -94,7 +90,7 @@ export async function updateQuestProgress(
return updates; return updates;
} catch (error) { } catch (error) {
logger.error(`Error updating quest progress for ${userId}:`, error); console.error(`Error updating quest progress for ${userId}:`, error);
return []; return [];
} }
} }
@@ -144,7 +140,7 @@ export async function claimQuestReward(
return { quest: progress.quest, rewards }; return { quest: progress.quest, rewards };
} catch (error) { } catch (error) {
logger.error(`Error claiming quest reward for ${userId}:`, error); console.error(`Error claiming quest reward for ${userId}:`, error);
throw error; throw error;
} }
} }
@@ -156,11 +152,7 @@ export async function getPlayerQuests(userId: string, guildId: string) {
const quests = await prisma.quest.findMany({ const quests = await prisma.quest.findMany({
where: { where: {
OR: [{ guildId }, { guildId: null }], OR: [{ guildId }, { guildId: null }],
active: true, active: true
OR: [
{ endAt: null },
{ endAt: { gte: new Date() } }
]
}, },
orderBy: [ orderBy: [
{ type: 'asc' }, { type: 'asc' },
@@ -272,10 +264,10 @@ export async function generateDailyQuests(guildId: string) {
}); });
} }
logger.info(`Generated ${selectedTemplates.length} daily quests for guild ${guildId}`); console.log(`Generated ${selectedTemplates.length} daily quests for guild ${guildId}`);
return selectedTemplates.length; return selectedTemplates.length;
} catch (error) { } catch (error) {
logger.error(`Error generating daily quests for ${guildId}:`, error); console.error(`Error generating daily quests for ${guildId}:`, error);
return 0; return 0;
} }
} }
@@ -295,6 +287,6 @@ export async function cleanExpiredQuests(guildId: string) {
} }
}); });
logger.info(`Deactivated ${result.count} expired quests for guild ${guildId}`); console.log(`Deactivated ${result.count} expired quests for guild ${guildId}`);
return result.count; return result.count;
} }

View File

@@ -52,15 +52,15 @@ export async function giveRewards(
guildId, guildId,
action: 'reward_given', action: 'reward_given',
target: source, target: source,
details: rewards details: rewards as any
} }
}).catch(() => {}); // Silencioso si falla el log }).catch(() => {}); // Silencioso si falla el log
logger.info(`Rewards given to ${userId} in ${guildId} from ${source}:`, rewards); console.log(`Rewards given to ${userId} in ${guildId} from ${source}:`, rewards);
return results; return results;
} catch (error) { } catch (error) {
logger.error(`Error giving rewards to ${userId} in ${guildId}:`, error); console.error(`Error giving rewards to ${userId} in ${guildId}:`, error);
throw error; throw error;
} }
} }

View File

@@ -69,7 +69,7 @@ export async function updateStats(
return stats; return stats;
} catch (error) { } catch (error) {
logger.error(`Error updating stats for ${userId} in ${guildId}:`, error); console.error(`Error updating stats for ${userId} in ${guildId}:`, error);
throw error; throw error;
} }
} }

View File

@@ -85,7 +85,7 @@ export async function updateStreak(userId: string, guildId: string) {
daysIncreased: daysDiff === 1 daysIncreased: daysDiff === 1
}; };
} catch (error) { } catch (error) {
logger.error(`Error updating streak for ${userId}:`, error); console.error(`Error updating streak for ${userId}:`, error);
throw error; throw error;
} }
} }