feat: agregar penalizaciones por derrota en minijuegos y mejorar la visualización de estadísticas del jugador
This commit is contained in:
@@ -4,6 +4,8 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"db:push": "prisma db push",
|
||||||
|
"db:pull": "prisma db pull",
|
||||||
"start": "npx tsx watch src/main.ts",
|
"start": "npx tsx watch src/main.ts",
|
||||||
"script:guild": "node scripts/setupGuildCacheCollection.js",
|
"script:guild": "node scripts/setupGuildCacheCollection.js",
|
||||||
"dev": "npx tsx watch src/main.ts",
|
"dev": "npx tsx watch src/main.ts",
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ import {
|
|||||||
getEquipment,
|
getEquipment,
|
||||||
getEffectiveStats,
|
getEffectiveStats,
|
||||||
} from "../../../game/combat/equipmentService";
|
} from "../../../game/combat/equipmentService";
|
||||||
import { getPlayerStatsFormatted } from "../../../game/stats/service";
|
import {
|
||||||
|
getPlayerStatsFormatted,
|
||||||
|
getOrCreatePlayerStats,
|
||||||
|
} from "../../../game/stats/service";
|
||||||
import type { TextBasedChannel } from "discord.js";
|
import type { TextBasedChannel } from "discord.js";
|
||||||
import { formatItemLabel } from "./_helpers";
|
import { formatItemLabel } from "./_helpers";
|
||||||
|
import { heartsBar } from "../../../game/lib/rpgFormat";
|
||||||
|
import { getActiveStatusEffects } from "../../../game/combat/statusEffectsService";
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: "player",
|
name: "player",
|
||||||
@@ -29,6 +34,12 @@ export const command: CommandMessage = {
|
|||||||
const { eq, weapon, armor, cape } = await getEquipment(userId, guildId);
|
const { eq, weapon, armor, cape } = await getEquipment(userId, guildId);
|
||||||
const stats = await getEffectiveStats(userId, guildId);
|
const stats = await getEffectiveStats(userId, guildId);
|
||||||
const playerStats = await getPlayerStatsFormatted(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
|
// Progreso por áreas
|
||||||
const progress = await prisma.playerProgress.findMany({
|
const progress = await prisma.playerProgress.findMany({
|
||||||
@@ -87,9 +98,12 @@ export const command: CommandMessage = {
|
|||||||
type: 10,
|
type: 10,
|
||||||
content:
|
content:
|
||||||
`**<:stats:1425689271788113991> ESTADÍSTICAS**\n` +
|
`**<:stats:1425689271788113991> ESTADÍSTICAS**\n` +
|
||||||
`<:healbonus:1425671499792121877> HP: **${stats.hp}/${stats.maxHp}**\n` +
|
`<:healbonus:1425671499792121877> HP: **${stats.hp}/${
|
||||||
`<:damage:1425670476449189998> ATK: **${stats.damage}**\n` +
|
stats.maxHp
|
||||||
|
}** ${heartsBar(stats.hp, stats.maxHp)}\n` +
|
||||||
|
`<:damage:1425670476449189998> ATK: **${stats.damage}** ${damageBonusDisplay}\n` +
|
||||||
`<:defens:1425670433910427862> DEF: **${stats.defense}**\n` +
|
`<:defens:1425670433910427862> DEF: **${stats.defense}**\n` +
|
||||||
|
`🏆 Racha: **${streak}** (mejor: ${rawStats.longestWinStreak})\n` +
|
||||||
`<a:9470coin:1425694135607885906> Monedas: **${wallet.coins.toLocaleString()}**`,
|
`<a:9470coin:1425694135607885906> Monedas: **${wallet.coins.toLocaleString()}**`,
|
||||||
},
|
},
|
||||||
{ type: 14, divider: true },
|
{ 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
|
// Añadir stats de actividades si existen
|
||||||
if (playerStats.activities) {
|
if (playerStats.activities) {
|
||||||
const activitiesText = Object.entries(playerStats.activities)
|
const activitiesText = Object.entries(playerStats.activities)
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ export function combatSummaryRPG(c: {
|
|||||||
outcome?: "victory" | "defeat";
|
outcome?: "victory" | "defeat";
|
||||||
maxRefHp?: number; // para cálculo visual si difiere
|
maxRefHp?: number; // para cálculo visual si difiere
|
||||||
autoDefeatNoWeapon?: boolean;
|
autoDefeatNoWeapon?: boolean;
|
||||||
|
deathPenalty?: {
|
||||||
|
goldLost?: number;
|
||||||
|
fatigueAppliedMinutes?: number;
|
||||||
|
fatigueMagnitude?: number;
|
||||||
|
};
|
||||||
}) {
|
}) {
|
||||||
const header = `**Combate (${outcomeLabel(c.outcome)})**`;
|
const header = `**Combate (${outcomeLabel(c.outcome)})**`;
|
||||||
const lines = [
|
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.`
|
`• 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) {
|
if (c.playerStartHp != null && c.playerEndHp != null) {
|
||||||
const maxHp = c.maxRefHp || Math.max(c.playerStartHp, c.playerEndHp);
|
const maxHp = c.maxRefHp || Math.max(c.playerStartHp, c.playerEndHp);
|
||||||
lines.push(
|
lines.push(
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { applyDeathFatigue } from "../combat/statusEffectsService";
|
||||||
|
import { getOrCreateWallet } from "../economy/service";
|
||||||
import { prisma } from "../../core/database/prisma";
|
import { prisma } from "../../core/database/prisma";
|
||||||
import {
|
import {
|
||||||
addItemByKey,
|
addItemByKey,
|
||||||
@@ -411,6 +413,49 @@ export async function runMinigame(
|
|||||||
where: { userId_guildId: { userId, guildId } },
|
where: { userId_guildId: { userId, guildId } },
|
||||||
data: { currentWinStreak: 0 },
|
data: { currentWinStreak: 0 },
|
||||||
});
|
});
|
||||||
|
// 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 = {
|
combatSummary = {
|
||||||
mobs: mobLogs,
|
mobs: mobLogs,
|
||||||
totalDamageDealt: 0,
|
totalDamageDealt: 0,
|
||||||
@@ -422,6 +467,7 @@ export async function runMinigame(
|
|||||||
outcome: "defeat",
|
outcome: "defeat",
|
||||||
autoDefeatNoWeapon: true,
|
autoDefeatNoWeapon: true,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let currentHp = startHp;
|
let currentHp = startHp;
|
||||||
const mobLogs: CombatSummary["mobs"] = [];
|
const mobLogs: CombatSummary["mobs"] = [];
|
||||||
@@ -524,13 +570,56 @@ export async function runMinigame(
|
|||||||
}
|
}
|
||||||
await updateStats(userId, guildId, statUpdates as any);
|
await updateStats(userId, guildId, statUpdates as any);
|
||||||
if (defeatedNow) {
|
if (defeatedNow) {
|
||||||
// reset de racha: update directo
|
|
||||||
await prisma.playerStats.update({
|
await prisma.playerStats.update({
|
||||||
where: { userId_guildId: { userId, guildId } },
|
where: { userId_guildId: { userId, guildId } },
|
||||||
data: { currentWinStreak: 0 },
|
data: { currentWinStreak: 0 },
|
||||||
});
|
});
|
||||||
} else if (victory) {
|
// Penalizaciones por derrota
|
||||||
// posible actualización de longestWinStreak si superada ya la maneja updateStats parcialmente; reforzar
|
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(
|
await prisma.$executeRawUnsafe(
|
||||||
`UPDATE "PlayerStats" SET "longestWinStreak" = GREATEST("longestWinStreak", "currentWinStreak") WHERE "userId" = $1 AND "guildId" = $2`,
|
`UPDATE "PlayerStats" SET "longestWinStreak" = GREATEST("longestWinStreak", "currentWinStreak") WHERE "userId" = $1 AND "guildId" = $2`,
|
||||||
userId,
|
userId,
|
||||||
@@ -549,6 +638,7 @@ export async function runMinigame(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Registrar la ejecución
|
// Registrar la ejecución
|
||||||
const resultJson: Prisma.InputJsonValue = {
|
const resultJson: Prisma.InputJsonValue = {
|
||||||
|
|||||||
@@ -93,4 +93,9 @@ export type CombatSummary = {
|
|||||||
playerEndHp?: number;
|
playerEndHp?: number;
|
||||||
outcome?: "victory" | "defeat";
|
outcome?: "victory" | "defeat";
|
||||||
autoDefeatNoWeapon?: boolean; // true si la derrota fue inmediata por no tener arma (damage <= 0)
|
autoDefeatNoWeapon?: boolean; // true si la derrota fue inmediata por no tener arma (damage <= 0)
|
||||||
|
deathPenalty?: {
|
||||||
|
goldLost?: number;
|
||||||
|
fatigueAppliedMinutes?: number;
|
||||||
|
fatigueMagnitude?: number; // 0.15 = 15%
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user