diff --git a/package.json b/package.json index f4ec7ae..ae9ad03 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "description": "", "main": "src/main.ts", "scripts": { + "db:push": "prisma db push", + "db:pull": "prisma db pull", "start": "npx tsx watch src/main.ts", "script:guild": "node scripts/setupGuildCacheCollection.js", "dev": "npx tsx watch src/main.ts", diff --git a/src/commands/messages/game/player.ts b/src/commands/messages/game/player.ts index afb0f36..c30250c 100644 --- a/src/commands/messages/game/player.ts +++ b/src/commands/messages/game/player.ts @@ -6,9 +6,14 @@ import { getEquipment, getEffectiveStats, } from "../../../game/combat/equipmentService"; -import { getPlayerStatsFormatted } from "../../../game/stats/service"; +import { + getPlayerStatsFormatted, + getOrCreatePlayerStats, +} from "../../../game/stats/service"; import type { TextBasedChannel } from "discord.js"; import { formatItemLabel } from "./_helpers"; +import { heartsBar } from "../../../game/lib/rpgFormat"; +import { getActiveStatusEffects } from "../../../game/combat/statusEffectsService"; export const command: CommandMessage = { name: "player", @@ -29,6 +34,12 @@ export const command: CommandMessage = { const { eq, weapon, armor, cape } = await getEquipment(userId, guildId); const stats = await getEffectiveStats(userId, guildId); const playerStats = await getPlayerStatsFormatted(userId, guildId); + const rawStats = await getOrCreatePlayerStats(userId, guildId); + const streak = rawStats.currentWinStreak; + const streakBonusPct = Math.min(Math.floor(streak / 3), 30); // cada 3 = 1%, mostramos valor base en % + const damageBonusDisplay = + streakBonusPct > 0 ? `(+${streakBonusPct}% racha)` : ""; + const effects = await getActiveStatusEffects(userId, guildId); // Progreso por áreas const progress = await prisma.playerProgress.findMany({ @@ -87,9 +98,12 @@ export const command: CommandMessage = { type: 10, content: `**<:stats:1425689271788113991> ESTADÍSTICAS**\n` + - `<:healbonus:1425671499792121877> HP: **${stats.hp}/${stats.maxHp}**\n` + - `<:damage:1425670476449189998> ATK: **${stats.damage}**\n` + + `<:healbonus:1425671499792121877> HP: **${stats.hp}/${ + stats.maxHp + }** ${heartsBar(stats.hp, stats.maxHp)}\n` + + `<:damage:1425670476449189998> ATK: **${stats.damage}** ${damageBonusDisplay}\n` + `<:defens:1425670433910427862> DEF: **${stats.defense}**\n` + + `🏆 Racha: **${streak}** (mejor: ${rawStats.longestWinStreak})\n` + ` Monedas: **${wallet.coins.toLocaleString()}**`, }, { type: 14, divider: true }, @@ -114,6 +128,37 @@ export const command: CommandMessage = { ], }; + // Añadir efectos activos (después de construir el bloque base para mantener orden) + if (effects.length > 0) { + const nowTs = Date.now(); + const fxLines = effects + .map((e) => { + let remain = ""; + if (e.expiresAt) { + const ms = e.expiresAt.getTime() - nowTs; + if (ms > 0) { + const m = Math.floor(ms / 60000); + const s = Math.floor((ms % 60000) / 1000); + remain = ` (${m}m ${s}s)`; + } else remain = " (exp)"; + } + switch (e.type) { + case "FATIGUE": { + const pct = Math.round(e.magnitude * 100); + return `• Fatiga: -${pct}% daño${remain}`; + } + default: + return `• ${e.type}${remain}`; + } + }) + .join("\n"); + display.components.push({ type: 14, divider: true }); + display.components.push({ + type: 10, + content: `**😵 EFECTOS ACTIVOS**\n${fxLines}`, + }); + } + // Añadir stats de actividades si existen if (playerStats.activities) { const activitiesText = Object.entries(playerStats.activities) diff --git a/src/game/lib/rpgFormat.ts b/src/game/lib/rpgFormat.ts index 7f37689..c608c50 100644 --- a/src/game/lib/rpgFormat.ts +++ b/src/game/lib/rpgFormat.ts @@ -83,6 +83,11 @@ export function combatSummaryRPG(c: { outcome?: "victory" | "defeat"; maxRefHp?: number; // para cálculo visual si difiere autoDefeatNoWeapon?: boolean; + deathPenalty?: { + goldLost?: number; + fatigueAppliedMinutes?: number; + fatigueMagnitude?: number; + }; }) { const header = `**Combate (${outcomeLabel(c.outcome)})**`; const lines = [ @@ -94,6 +99,21 @@ export function combatSummaryRPG(c: { `• Derrota automática: no tenías arma equipada o válida (daño 0). Equipa un arma para poder atacar.` ); } + if (c.deathPenalty) { + const parts: string[] = []; + if ( + typeof c.deathPenalty.goldLost === "number" && + c.deathPenalty.goldLost > 0 + ) + parts.push(`-${c.deathPenalty.goldLost} monedas`); + if (c.deathPenalty.fatigueAppliedMinutes) { + const pct = c.deathPenalty.fatigueMagnitude + ? Math.round(c.deathPenalty.fatigueMagnitude * 100) + : 15; + parts.push(`Fatiga ${pct}% ${c.deathPenalty.fatigueAppliedMinutes}m`); + } + if (parts.length) lines.push(`• Penalización: ${parts.join(" | ")}`); + } if (c.playerStartHp != null && c.playerEndHp != null) { const maxHp = c.maxRefHp || Math.max(c.playerStartHp, c.playerEndHp); lines.push( diff --git a/src/game/minigames/service.ts b/src/game/minigames/service.ts index 50d9e28..2c2258e 100644 --- a/src/game/minigames/service.ts +++ b/src/game/minigames/service.ts @@ -1,3 +1,5 @@ +import { applyDeathFatigue } from "../combat/statusEffectsService"; +import { getOrCreateWallet } from "../economy/service"; import { prisma } from "../../core/database/prisma"; import { addItemByKey, @@ -411,17 +413,61 @@ export async function runMinigame( where: { userId_guildId: { userId, guildId } }, data: { currentWinStreak: 0 }, }); - combatSummary = { - mobs: mobLogs, - totalDamageDealt: 0, - totalDamageTaken: 0, - mobsDefeated: 0, - victory: false, - playerStartHp: startHp, - playerEndHp: endHp, - outcome: "defeat", - autoDefeatNoWeapon: true, - }; + // Penalizaciones por derrota: pérdida de oro + fatiga + let deathPenalty: CombatSummary["deathPenalty"] | undefined; + try { + const wallet = await getOrCreateWallet(userId, guildId); + const coins = wallet.coins; + let goldLost = 0; + if (coins > 0) { + goldLost = Math.floor(coins * 0.05); // 5% + if (goldLost < 1) goldLost = 1; + if (goldLost > 500) goldLost = 500; // cap + if (goldLost > 0) { + await prisma.economyWallet.update({ + where: { userId_guildId: { userId, guildId } }, + data: { coins: { decrement: goldLost } }, + }); + } + } + const fatigueMagnitude = 0.15; + const fatigueMinutes = 5; + await applyDeathFatigue( + userId, + guildId, + fatigueMagnitude, + fatigueMinutes + ); + deathPenalty = { + goldLost, + fatigueAppliedMinutes: fatigueMinutes, + fatigueMagnitude, + }; + combatSummary = { + mobs: mobLogs, + totalDamageDealt: 0, + totalDamageTaken: 0, + mobsDefeated: 0, + victory: false, + playerStartHp: startHp, + playerEndHp: endHp, + outcome: "defeat", + autoDefeatNoWeapon: true, + deathPenalty, + }; + } catch { + combatSummary = { + mobs: mobLogs, + totalDamageDealt: 0, + totalDamageTaken: 0, + mobsDefeated: 0, + victory: false, + playerStartHp: startHp, + playerEndHp: endHp, + outcome: "defeat", + autoDefeatNoWeapon: true, + }; + } } else { let currentHp = startHp; const mobLogs: CombatSummary["mobs"] = []; @@ -524,29 +570,73 @@ export async function runMinigame( } await updateStats(userId, guildId, statUpdates as any); if (defeatedNow) { - // reset de racha: update directo await prisma.playerStats.update({ where: { userId_guildId: { userId, guildId } }, data: { currentWinStreak: 0 }, }); - } else if (victory) { - // posible actualización de longestWinStreak si superada ya la maneja updateStats parcialmente; reforzar - await prisma.$executeRawUnsafe( - `UPDATE "PlayerStats" SET "longestWinStreak" = GREATEST("longestWinStreak", "currentWinStreak") WHERE "userId" = $1 AND "guildId" = $2`, - userId, - guildId - ); + // Penalizaciones por derrota + let deathPenalty: CombatSummary["deathPenalty"] | undefined; + try { + const wallet = await getOrCreateWallet(userId, guildId); + const coins = wallet.coins; + let goldLost = 0; + if (coins > 0) { + goldLost = Math.floor(coins * 0.05); + if (goldLost < 1) goldLost = 1; + if (goldLost > 500) goldLost = 500; + if (goldLost > 0) { + await prisma.economyWallet.update({ + where: { userId_guildId: { userId, guildId } }, + data: { coins: { decrement: goldLost } }, + }); + } + } + const fatigueMagnitude = 0.15; + const fatigueMinutes = 5; + await applyDeathFatigue( + userId, + guildId, + fatigueMagnitude, + fatigueMinutes + ); + deathPenalty = { + goldLost, + fatigueAppliedMinutes: fatigueMinutes, + fatigueMagnitude, + }; + } catch { + // silencioso + } + combatSummary = { + mobs: mobLogs, + totalDamageDealt: totalDealt, + totalDamageTaken: totalTaken, + mobsDefeated: totalMobsDefeated, + victory, + playerStartHp: startHp, + playerEndHp: endHp, + outcome: "defeat", + deathPenalty, + }; + } else { + if (victory) { + await prisma.$executeRawUnsafe( + `UPDATE "PlayerStats" SET "longestWinStreak" = GREATEST("longestWinStreak", "currentWinStreak") WHERE "userId" = $1 AND "guildId" = $2`, + userId, + guildId + ); + } + combatSummary = { + mobs: mobLogs, + totalDamageDealt: totalDealt, + totalDamageTaken: totalTaken, + mobsDefeated: totalMobsDefeated, + victory, + playerStartHp: startHp, + playerEndHp: endHp, + outcome: victory ? "victory" : "defeat", + }; } - combatSummary = { - mobs: mobLogs, - totalDamageDealt: totalDealt, - totalDamageTaken: totalTaken, - mobsDefeated: totalMobsDefeated, - victory, - playerStartHp: startHp, - playerEndHp: endHp, - outcome: victory ? "victory" : "defeat", - }; } } diff --git a/src/game/minigames/types.ts b/src/game/minigames/types.ts index e454327..b4729f1 100644 --- a/src/game/minigames/types.ts +++ b/src/game/minigames/types.ts @@ -93,4 +93,9 @@ export type CombatSummary = { playerEndHp?: number; outcome?: "victory" | "defeat"; autoDefeatNoWeapon?: boolean; // true si la derrota fue inmediata por no tener arma (damage <= 0) + deathPenalty?: { + goldLost?: number; + fatigueAppliedMinutes?: number; + fatigueMagnitude?: number; // 0.15 = 15% + }; };