feat(economy): implement player profile command with detailed stats and inventory information
This commit is contained in:
215
src/commands/messages/game/player.ts
Normal file
215
src/commands/messages/game/player.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { prisma } from '../../../core/database/prisma';
|
||||
import { getOrCreateWallet } from '../../../game/economy/service';
|
||||
import { getEquipment, getEffectiveStats } from '../../../game/combat/equipmentService';
|
||||
import type { ItemProps } from '../../../game/economy/types';
|
||||
|
||||
function parseItemProps(json: unknown): ItemProps {
|
||||
if (!json || typeof json !== 'object') return {};
|
||||
return json as ItemProps;
|
||||
}
|
||||
|
||||
function fmtTool(props: ItemProps) {
|
||||
const t = props.tool;
|
||||
if (!t) return '';
|
||||
const icon = t.type === 'pickaxe' ? '⛏️' : t.type === 'rod' ? '🎣' : t.type === 'sword' ? '🗡️' : t.type === 'bow' ? '🏹' : t.type === 'halberd' ? '⚔️' : t.type === 'net' ? '🕸️' : '🔧';
|
||||
const tier = t.tier != null ? ` T${t.tier}` : '';
|
||||
return `${icon}${tier}`;
|
||||
}
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'player',
|
||||
type: 'message',
|
||||
aliases: ['perfil', 'profile', 'yo', 'me'],
|
||||
cooldown: 5,
|
||||
description: 'Muestra toda tu información de jugador: stats, equipo, progreso y últimas actividades.',
|
||||
usage: 'player [@usuario]',
|
||||
run: async (message, args, _client: Amayo) => {
|
||||
// Permitir ver perfil de otros usuarios mencionándolos
|
||||
const targetUser = message.mentions.users.first() || message.author;
|
||||
const userId = targetUser.id;
|
||||
const guildId = message.guild!.id;
|
||||
|
||||
// Obtener datos del jugador
|
||||
const wallet = await getOrCreateWallet(userId, guildId);
|
||||
const { eq, weapon, armor, cape } = await getEquipment(userId, guildId);
|
||||
const stats = await getEffectiveStats(userId, guildId);
|
||||
const playerState = await prisma.playerState.findUnique({ where: { userId_guildId: { userId, guildId } } });
|
||||
|
||||
// Progreso por áreas
|
||||
const progress = await prisma.playerProgress.findMany({
|
||||
where: { userId, guildId },
|
||||
include: { area: true },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
take: 5,
|
||||
});
|
||||
|
||||
// Últimas actividades de minijuegos
|
||||
const recentRuns = await prisma.minigameRun.findMany({
|
||||
where: { userId, guildId },
|
||||
include: { area: true },
|
||||
orderBy: { startedAt: 'desc' },
|
||||
take: 5,
|
||||
});
|
||||
|
||||
// Conteo de items en inventario
|
||||
const inventoryCount = await prisma.inventoryEntry.count({
|
||||
where: { userId, guildId, quantity: { gt: 0 } },
|
||||
});
|
||||
|
||||
// Total de items (cantidad sumada)
|
||||
const inventorySum = await prisma.inventoryEntry.aggregate({
|
||||
where: { userId, guildId },
|
||||
_sum: { quantity: true },
|
||||
});
|
||||
|
||||
// Compras totales
|
||||
const purchaseCount = await prisma.shopPurchase.count({
|
||||
where: { userId, guildId },
|
||||
});
|
||||
|
||||
// Cooldowns activos
|
||||
const activeCooldowns = await prisma.actionCooldown.findMany({
|
||||
where: { userId, guildId, until: { gt: new Date() } },
|
||||
orderBy: { until: 'asc' },
|
||||
take: 5,
|
||||
});
|
||||
|
||||
// Construir el mensaje
|
||||
const lines: string[] = [];
|
||||
|
||||
// Header
|
||||
lines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
||||
lines.push(`👤 **Perfil de ${targetUser.username}**`);
|
||||
lines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
||||
|
||||
// 📊 Stats Básicos
|
||||
lines.push(`📊 **ESTADÍSTICAS**`);
|
||||
lines.push(`❤️ HP: ${stats.hp}/${stats.maxHp}`);
|
||||
lines.push(`⚔️ ATK: ${stats.damage}`);
|
||||
lines.push(`🛡️ DEF: ${stats.defense}`);
|
||||
lines.push(`💰 Monedas: ${wallet.coins}`);
|
||||
lines.push(``);
|
||||
|
||||
// 🎒 Inventario
|
||||
lines.push(`🎒 **INVENTARIO**`);
|
||||
lines.push(`📦 Items únicos: ${inventoryCount}`);
|
||||
lines.push(`🔢 Total items: ${inventorySum._sum.quantity ?? 0}`);
|
||||
lines.push(`🛒 Compras totales: ${purchaseCount}`);
|
||||
lines.push(``);
|
||||
|
||||
// 🧰 Equipamiento
|
||||
lines.push(`🧰 **EQUIPAMIENTO**`);
|
||||
if (weapon) {
|
||||
const wProps = parseItemProps(weapon.props);
|
||||
const wTool = fmtTool(wProps);
|
||||
const wDmg = wProps.damage ? ` +${wProps.damage} ATK` : '';
|
||||
lines.push(`🗡️ Arma: ${weapon.name || weapon.key}${wTool ? ` ${wTool}` : ''}${wDmg}`);
|
||||
} else {
|
||||
lines.push(`🗡️ Arma: -`);
|
||||
}
|
||||
|
||||
if (armor) {
|
||||
const aProps = parseItemProps(armor.props);
|
||||
const aDef = aProps.defense ? ` +${aProps.defense} DEF` : '';
|
||||
lines.push(`🛡️ Armadura: ${armor.name || armor.key}${aDef}`);
|
||||
} else {
|
||||
lines.push(`🛡️ Armadura: -`);
|
||||
}
|
||||
|
||||
if (cape) {
|
||||
const cProps = parseItemProps(cape.props);
|
||||
const cHp = cProps.maxHpBonus ? ` +${cProps.maxHpBonus} HP` : '';
|
||||
lines.push(`🧥 Capa: ${cape.name || cape.key}${cHp}`);
|
||||
} else {
|
||||
lines.push(`🧥 Capa: -`);
|
||||
}
|
||||
lines.push(``);
|
||||
|
||||
// 🗺️ Progreso por Áreas
|
||||
if (progress.length > 0) {
|
||||
lines.push(`🗺️ **PROGRESO EN ÁREAS**`);
|
||||
for (const p of progress) {
|
||||
const areaIcon = p.area.type === 'MINE' ? '⛏️' : p.area.type === 'LAGOON' ? '🎣' : p.area.type === 'FIGHT' ? '⚔️' : p.area.type === 'FARM' ? '🌾' : '🗺️';
|
||||
lines.push(`${areaIcon} ${p.area.name}: Nivel ${p.highestLevel}`);
|
||||
}
|
||||
lines.push(``);
|
||||
}
|
||||
|
||||
// 📜 Últimas Actividades
|
||||
if (recentRuns.length > 0) {
|
||||
lines.push(`📜 **ÚLTIMAS ACTIVIDADES**`);
|
||||
for (const run of recentRuns.slice(0, 3)) {
|
||||
const result = run.result as any;
|
||||
const areaIcon = run.area.type === 'MINE' ? '⛏️' : run.area.type === 'LAGOON' ? '🎣' : run.area.type === 'FIGHT' ? '⚔️' : run.area.type === 'FARM' ? '🌾' : '🗺️';
|
||||
const timestamp = run.startedAt;
|
||||
const relativeTime = getRelativeTime(timestamp);
|
||||
const rewardsCount = result.rewards?.length ?? 0;
|
||||
lines.push(`${areaIcon} ${run.area.name} (Nv.${run.level}) - ${rewardsCount} recompensas - ${relativeTime}`);
|
||||
}
|
||||
lines.push(``);
|
||||
}
|
||||
|
||||
// ⏱️ Cooldowns Activos
|
||||
if (activeCooldowns.length > 0) {
|
||||
lines.push(`⏱️ **COOLDOWNS ACTIVOS**`);
|
||||
for (const cd of activeCooldowns) {
|
||||
const remaining = Math.max(0, Math.ceil((cd.until.getTime() - Date.now()) / 1000));
|
||||
const cdName = formatCooldownKey(cd.key);
|
||||
lines.push(`⏳ ${cdName}: ${formatDuration(remaining)}`);
|
||||
}
|
||||
lines.push(``);
|
||||
}
|
||||
|
||||
// Stats adicionales del PlayerState
|
||||
if (playerState?.stats) {
|
||||
const additionalStats = playerState.stats as any;
|
||||
if (Object.keys(additionalStats).length > 0) {
|
||||
lines.push(`🎯 **STATS ADICIONALES**`);
|
||||
if (additionalStats.attack != null) lines.push(`⚔️ Ataque Base: ${additionalStats.attack}`);
|
||||
if (additionalStats.defense != null) lines.push(`🛡️ Defensa Base: ${additionalStats.defense}`);
|
||||
if (additionalStats.strength != null) lines.push(`💪 Fuerza: ${additionalStats.strength}`);
|
||||
if (additionalStats.luck != null) lines.push(`🍀 Suerte: ${additionalStats.luck}`);
|
||||
lines.push(``);
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
lines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
||||
lines.push(`Usa \`!inventario\` para ver tus items completos`);
|
||||
|
||||
await message.reply(lines.join('\n'));
|
||||
},
|
||||
};
|
||||
|
||||
// Helpers
|
||||
function getRelativeTime(date: Date): string {
|
||||
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
||||
if (seconds < 60) return 'Hace un momento';
|
||||
if (seconds < 3600) return `Hace ${Math.floor(seconds / 60)}m`;
|
||||
if (seconds < 86400) return `Hace ${Math.floor(seconds / 3600)}h`;
|
||||
if (seconds < 604800) return `Hace ${Math.floor(seconds / 86400)}d`;
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const mins = Math.floor((seconds % 3600) / 60);
|
||||
return `${hours}h ${mins}m`;
|
||||
}
|
||||
|
||||
function formatCooldownKey(key: string): string {
|
||||
// Convertir keys como "minigame:mine.starter" a "Mina"
|
||||
if (key.startsWith('minigame:')) {
|
||||
const areaKey = key.split(':')[1];
|
||||
if (areaKey?.includes('mine')) return 'Mina';
|
||||
if (areaKey?.includes('lagoon')) return 'Laguna';
|
||||
if (areaKey?.includes('fight')) return 'Pelea';
|
||||
if (areaKey?.includes('farm')) return 'Granja';
|
||||
return areaKey || key;
|
||||
}
|
||||
return key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
}
|
||||
Reference in New Issue
Block a user