feat: add TypeScript type checking task to VSCode configuration
feat: implement area metadata blocks in game commands for enhanced area details fix: refactor game commands to utilize new area metadata blocks and improve code consistency feat: enhance Appwrite API integration with additional collections and storage support refactor: update componentsV2 to support image blocks in display rendering
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { prisma } from "../../../core/database/prisma";
|
||||
import { textBlock, dividerBlock } from "../../../core/lib/componentsV2";
|
||||
import type { GameArea } from "@prisma/client";
|
||||
import type { ItemProps } from "../../../game/economy/types";
|
||||
import type {
|
||||
@@ -209,6 +210,44 @@ export async function fetchItemBasics(
|
||||
return result;
|
||||
}
|
||||
|
||||
export type AreaMetadata =
|
||||
| {
|
||||
previewImage?: string;
|
||||
image?: string;
|
||||
referenceImage?: string;
|
||||
description?: string;
|
||||
[k: string]: any;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
export function buildAreaMetadataBlocks(
|
||||
area: Pick<GameArea, "metadata" | "key" | "name">
|
||||
) {
|
||||
const blocks: any[] = [];
|
||||
const meta = (area.metadata as AreaMetadata) || undefined;
|
||||
if (!meta) return blocks;
|
||||
|
||||
const img = meta.previewImage || meta.image || meta.referenceImage;
|
||||
const desc =
|
||||
typeof meta.description === "string" && meta.description.trim().length > 0
|
||||
? meta.description.trim()
|
||||
: null;
|
||||
|
||||
if (desc) {
|
||||
blocks.push(textBlock(`**🗺️ Detalles del área**\n${desc}`));
|
||||
}
|
||||
if (img && typeof img === "string") {
|
||||
// Mostrar también como texto para compatibilidad, y dejar que el renderer agregue imagen si soporta
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
blocks.push(textBlock(`**🖼️ Mapa/Imagen:** ${img}`));
|
||||
// Si el renderer soporta bloque de imagen, los consumidores podrán usarlo
|
||||
// @ts-ignore: el builder acepta bloques extendidos
|
||||
blocks.push({ kind: "image", url: img });
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
export interface KeyPickerOption {
|
||||
value: string;
|
||||
label: string;
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { runMinigame } from '../../../game/minigames/service';
|
||||
import { getDefaultLevel, findBestToolKey, parseGameArgs, resolveGuildAreaWithFallback, resolveAreaByType, sendDisplayReply, fetchItemBasics, formatItemLabel } from './_helpers';
|
||||
import { updateStats } from '../../../game/stats/service';
|
||||
import { updateQuestProgress } from '../../../game/quests/service';
|
||||
import { checkAchievements } from '../../../game/achievements/service';
|
||||
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
||||
import type { CommandMessage } from "../../../core/types/commands";
|
||||
import type Amayo from "../../../core/client";
|
||||
import { runMinigame } from "../../../game/minigames/service";
|
||||
import {
|
||||
getDefaultLevel,
|
||||
findBestToolKey,
|
||||
parseGameArgs,
|
||||
resolveGuildAreaWithFallback,
|
||||
resolveAreaByType,
|
||||
sendDisplayReply,
|
||||
fetchItemBasics,
|
||||
formatItemLabel,
|
||||
} from "./_helpers";
|
||||
import { updateStats } from "../../../game/stats/service";
|
||||
import { updateQuestProgress } from "../../../game/quests/service";
|
||||
import { checkAchievements } from "../../../game/achievements/service";
|
||||
import {
|
||||
buildDisplay,
|
||||
dividerBlock,
|
||||
textBlock,
|
||||
} from "../../../core/lib/componentsV2";
|
||||
import { buildAreaMetadataBlocks } from "./_helpers";
|
||||
|
||||
const MINING_ACCENT = 0xC27C0E;
|
||||
const MINING_ACCENT = 0xc27c0e;
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'mina',
|
||||
type: 'message',
|
||||
aliases: ['minar'],
|
||||
name: "mina",
|
||||
type: "message",
|
||||
aliases: ["minar"],
|
||||
cooldown: 5,
|
||||
description: 'Ir a la mina (usa pico si está disponible) y obtener recompensas según el nivel.',
|
||||
usage: 'mina [nivel] [toolKey] [area:clave] (ej: mina 2 tool.pickaxe.basic)',
|
||||
description:
|
||||
"Ir a la mina (usa pico si está disponible) y obtener recompensas según el nivel.",
|
||||
usage: "mina [nivel] [toolKey] [area:clave] (ej: mina 2 tool.pickaxe.basic)",
|
||||
run: async (message, args, _client: Amayo) => {
|
||||
const userId = message.author.id;
|
||||
const guildId = message.guild!.id;
|
||||
@@ -23,59 +38,89 @@ export const command: CommandMessage = {
|
||||
|
||||
const areaInfo = areaOverride
|
||||
? await resolveGuildAreaWithFallback(guildId, areaOverride)
|
||||
: await resolveAreaByType(guildId, 'MINE');
|
||||
: await resolveAreaByType(guildId, "MINE");
|
||||
|
||||
if (!areaInfo.area) {
|
||||
if (areaOverride) {
|
||||
await message.reply(`⚠️ No existe un área con key \`${areaOverride}\` para este servidor.`);
|
||||
await message.reply(
|
||||
`⚠️ No existe un área con key \`${areaOverride}\` para este servidor.`
|
||||
);
|
||||
} else {
|
||||
await message.reply('⚠️ No hay un área de tipo **MINE** configurada. Crea una con `!area-crear` o especifica `area:<key>`.');
|
||||
await message.reply(
|
||||
"⚠️ No hay un área de tipo **MINE** configurada. Crea una con `!area-crear` o especifica `area:<key>`."
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { area, source } = areaInfo;
|
||||
const globalNotice = source === 'global'
|
||||
? `ℹ️ Usando configuración global para \`${area.key}\`. Puedes crear \`gameArea\` tipo **MINE** para personalizarla en este servidor.`
|
||||
: null;
|
||||
const globalNotice =
|
||||
source === "global"
|
||||
? `ℹ️ Usando configuración global para \`${area.key}\`. Puedes crear \`gameArea\` tipo **MINE** para personalizarla en este servidor.`
|
||||
: null;
|
||||
|
||||
const level = levelArg ?? await getDefaultLevel(userId, guildId, area.id);
|
||||
const toolKey = providedTool ?? await findBestToolKey(userId, guildId, 'pickaxe');
|
||||
const level = levelArg ?? (await getDefaultLevel(userId, guildId, area.id));
|
||||
const toolKey =
|
||||
providedTool ?? (await findBestToolKey(userId, guildId, "pickaxe"));
|
||||
|
||||
try {
|
||||
const result = await runMinigame(userId, guildId, area.key, level, { toolKey: toolKey ?? undefined });
|
||||
const result = await runMinigame(userId, guildId, area.key, level, {
|
||||
toolKey: toolKey ?? undefined,
|
||||
});
|
||||
|
||||
const rewardKeys = result.rewards
|
||||
.filter((r): r is { type: 'item'; itemKey: string; qty?: number } => r.type === 'item' && Boolean(r.itemKey))
|
||||
.filter(
|
||||
(r): r is { type: "item"; itemKey: string; qty?: number } =>
|
||||
r.type === "item" && Boolean(r.itemKey)
|
||||
)
|
||||
.map((r) => r.itemKey!);
|
||||
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||
|
||||
|
||||
// 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 rewardLines = result.rewards.length
|
||||
? result.rewards.map((r) => {
|
||||
if (r.type === 'coins') return `• 🪙 +${r.amount}`;
|
||||
const info = rewardItems.get(r.itemKey!);
|
||||
const label = formatItemLabel(info ?? { key: r.itemKey!, name: null, icon: null });
|
||||
return `• ${label} x${r.qty ?? 1}`;
|
||||
}).join('\n')
|
||||
: '• —';
|
||||
const mobsLines = result.mobs.length
|
||||
? result.mobs.map(m => `• ${m}`).join('\n')
|
||||
: '• —';
|
||||
const toolInfo = result.tool?.key
|
||||
? `${formatItemLabel(rewardItems.get(result.tool.key) ?? { key: result.tool.key, name: null, icon: null }, { fallbackIcon: '🔧' })}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta ?? 0} dur.)`}`
|
||||
: '—';
|
||||
await updateQuestProgress(userId, guildId, "mine_count", 1);
|
||||
|
||||
const blocks = [textBlock('# ⛏️ Mina')];
|
||||
// Verificar logros
|
||||
const newAchievements = await checkAchievements(
|
||||
userId,
|
||||
guildId,
|
||||
"mine_count"
|
||||
);
|
||||
|
||||
const rewardLines = result.rewards.length
|
||||
? result.rewards
|
||||
.map((r) => {
|
||||
if (r.type === "coins") return `• 🪙 +${r.amount}`;
|
||||
const info = rewardItems.get(r.itemKey!);
|
||||
const label = formatItemLabel(
|
||||
info ?? { key: r.itemKey!, name: null, icon: null }
|
||||
);
|
||||
return `• ${label} x${r.qty ?? 1}`;
|
||||
})
|
||||
.join("\n")
|
||||
: "• —";
|
||||
const mobsLines = result.mobs.length
|
||||
? result.mobs.map((m) => `• ${m}`).join("\n")
|
||||
: "• —";
|
||||
const toolInfo = result.tool?.key
|
||||
? `${formatItemLabel(
|
||||
rewardItems.get(result.tool.key) ?? {
|
||||
key: result.tool.key,
|
||||
name: null,
|
||||
icon: null,
|
||||
},
|
||||
{ fallbackIcon: "🔧" }
|
||||
)}${
|
||||
result.tool.broken
|
||||
? " (rota)"
|
||||
: ` (-${result.tool.durabilityDelta ?? 0} dur.)`
|
||||
}`
|
||||
: "—";
|
||||
|
||||
const blocks = [textBlock("# ⛏️ Mina")];
|
||||
|
||||
if (globalNotice) {
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
@@ -83,16 +128,32 @@ export const command: CommandMessage = {
|
||||
}
|
||||
|
||||
blocks.push(dividerBlock());
|
||||
const areaScope = source === 'global' ? '🌐 Configuración global' : '📍 Configuración local';
|
||||
blocks.push(textBlock(`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`));
|
||||
const areaScope =
|
||||
source === "global"
|
||||
? "🌐 Configuración global"
|
||||
: "📍 Configuración local";
|
||||
blocks.push(
|
||||
textBlock(
|
||||
`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`
|
||||
)
|
||||
);
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
blocks.push(textBlock(`**Recompensas**\n${rewardLines}`));
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
blocks.push(textBlock(`**Mobs**\n${mobsLines}`));
|
||||
|
||||
// Añadir metadata del área (imagen/descripcion) si existe
|
||||
const metaBlocks = buildAreaMetadataBlocks(area);
|
||||
if (metaBlocks.length) {
|
||||
blocks.push(dividerBlock());
|
||||
blocks.push(...metaBlocks);
|
||||
}
|
||||
|
||||
if (newAchievements.length > 0) {
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 2 }));
|
||||
const achLines = newAchievements.map(ach => `✨ **${ach.name}** — ${ach.description}`).join('\n');
|
||||
const achLines = newAchievements
|
||||
.map((ach) => `✨ **${ach.name}** — ${ach.description}`)
|
||||
.join("\n");
|
||||
blocks.push(textBlock(`🏆 ¡Logro desbloqueado!\n${achLines}`));
|
||||
}
|
||||
|
||||
@@ -101,6 +162,5 @@ export const command: CommandMessage = {
|
||||
} catch (e: any) {
|
||||
await message.reply(`❌ No se pudo minar: ${e?.message ?? e}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { runMinigame } from '../../../game/minigames/service';
|
||||
import { getDefaultLevel, findBestToolKey, parseGameArgs, resolveGuildAreaWithFallback, resolveAreaByType, sendDisplayReply, fetchItemBasics, formatItemLabel } from './_helpers';
|
||||
import { updateStats } from '../../../game/stats/service';
|
||||
import { updateQuestProgress } from '../../../game/quests/service';
|
||||
import { checkAchievements } from '../../../game/achievements/service';
|
||||
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
||||
import type { CommandMessage } from "../../../core/types/commands";
|
||||
import type Amayo from "../../../core/client";
|
||||
import { runMinigame } from "../../../game/minigames/service";
|
||||
import {
|
||||
getDefaultLevel,
|
||||
findBestToolKey,
|
||||
parseGameArgs,
|
||||
resolveGuildAreaWithFallback,
|
||||
resolveAreaByType,
|
||||
sendDisplayReply,
|
||||
fetchItemBasics,
|
||||
formatItemLabel,
|
||||
} from "./_helpers";
|
||||
import { updateStats } from "../../../game/stats/service";
|
||||
import { updateQuestProgress } from "../../../game/quests/service";
|
||||
import { checkAchievements } from "../../../game/achievements/service";
|
||||
import {
|
||||
buildDisplay,
|
||||
dividerBlock,
|
||||
textBlock,
|
||||
} from "../../../core/lib/componentsV2";
|
||||
import { buildAreaMetadataBlocks } from "./_helpers";
|
||||
|
||||
const FIGHT_ACCENT = 0x992D22;
|
||||
const FIGHT_ACCENT = 0x992d22;
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'pelear',
|
||||
type: 'message',
|
||||
aliases: ['fight','arena'],
|
||||
name: "pelear",
|
||||
type: "message",
|
||||
aliases: ["fight", "arena"],
|
||||
cooldown: 8,
|
||||
description: 'Entra a la arena y pelea (usa espada si está disponible).',
|
||||
usage: 'pelear [nivel] [toolKey] [area:clave] (ej: pelear 1 weapon.sword.iron)',
|
||||
description: "Entra a la arena y pelea (usa espada si está disponible).",
|
||||
usage:
|
||||
"pelear [nivel] [toolKey] [area:clave] (ej: pelear 1 weapon.sword.iron)",
|
||||
run: async (message, args, _client: Amayo) => {
|
||||
const userId = message.author.id;
|
||||
const guildId = message.guild!.id;
|
||||
@@ -23,63 +38,98 @@ export const command: CommandMessage = {
|
||||
|
||||
const areaInfo = areaOverride
|
||||
? await resolveGuildAreaWithFallback(guildId, areaOverride)
|
||||
: await resolveAreaByType(guildId, 'FIGHT');
|
||||
: await resolveAreaByType(guildId, "FIGHT");
|
||||
|
||||
if (!areaInfo.area) {
|
||||
if (areaOverride) {
|
||||
await message.reply(`⚠️ No existe un área con key \`${areaOverride}\` para este servidor.`);
|
||||
await message.reply(
|
||||
`⚠️ No existe un área con key \`${areaOverride}\` para este servidor.`
|
||||
);
|
||||
} else {
|
||||
await message.reply('⚠️ No hay un área de tipo **FIGHT** configurada. Crea una con `!area-crear` o especifica `area:<key>`.');
|
||||
await message.reply(
|
||||
"⚠️ No hay un área de tipo **FIGHT** configurada. Crea una con `!area-crear` o especifica `area:<key>`."
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { area, source } = areaInfo;
|
||||
const globalNotice = source === 'global'
|
||||
? `ℹ️ Usando configuración global para \`${area.key}\`. Puedes crear \`gameArea\` tipo **FIGHT** para personalizarla en este servidor.`
|
||||
: null;
|
||||
const globalNotice =
|
||||
source === "global"
|
||||
? `ℹ️ Usando configuración global para \`${area.key}\`. Puedes crear \`gameArea\` tipo **FIGHT** para personalizarla en este servidor.`
|
||||
: null;
|
||||
|
||||
const level = levelArg ?? await getDefaultLevel(userId, guildId, area.id);
|
||||
const toolKey = providedTool ?? await findBestToolKey(userId, guildId, 'sword');
|
||||
const level = levelArg ?? (await getDefaultLevel(userId, guildId, area.id));
|
||||
const toolKey =
|
||||
providedTool ?? (await findBestToolKey(userId, guildId, "sword"));
|
||||
|
||||
try {
|
||||
const result = await runMinigame(userId, guildId, area.key, level, { toolKey: toolKey ?? undefined });
|
||||
const result = await runMinigame(userId, guildId, area.key, level, {
|
||||
toolKey: toolKey ?? undefined,
|
||||
});
|
||||
|
||||
const rewardKeys = result.rewards
|
||||
.filter((r): r is { type: 'item'; itemKey: string; qty?: number } => r.type === 'item' && Boolean(r.itemKey))
|
||||
.filter(
|
||||
(r): r is { type: "item"; itemKey: string; qty?: number } =>
|
||||
r.type === "item" && Boolean(r.itemKey)
|
||||
)
|
||||
.map((r) => r.itemKey!);
|
||||
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||
|
||||
// Actualizar stats y misiones
|
||||
await updateStats(userId, guildId, { fightsCompleted: 1 });
|
||||
await updateQuestProgress(userId, guildId, 'fight_count', 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);
|
||||
await updateQuestProgress(
|
||||
userId,
|
||||
guildId,
|
||||
"mob_defeat_count",
|
||||
mobsCount
|
||||
);
|
||||
}
|
||||
|
||||
const newAchievements = await checkAchievements(userId, guildId, 'fight_count');
|
||||
|
||||
const rewardLines = result.rewards.length
|
||||
? result.rewards.map((r) => {
|
||||
if (r.type === 'coins') return `• 🪙 +${r.amount}`;
|
||||
const info = rewardItems.get(r.itemKey!);
|
||||
const label = formatItemLabel(info ?? { key: r.itemKey!, name: null, icon: null });
|
||||
return `• ${label} x${r.qty ?? 1}`;
|
||||
}).join('\n')
|
||||
: '• —';
|
||||
const mobsLines = result.mobs.length
|
||||
? result.mobs.map(m => `• ${m}`).join('\n')
|
||||
: '• —';
|
||||
const toolInfo = result.tool?.key
|
||||
? `${formatItemLabel(rewardItems.get(result.tool.key) ?? { key: result.tool.key, name: null, icon: null }, { fallbackIcon: '🗡️' })}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta ?? 0} dur.)`}`
|
||||
: '—';
|
||||
|
||||
const blocks = [textBlock('# ⚔️ Arena')];
|
||||
const newAchievements = await checkAchievements(
|
||||
userId,
|
||||
guildId,
|
||||
"fight_count"
|
||||
);
|
||||
|
||||
const rewardLines = result.rewards.length
|
||||
? result.rewards
|
||||
.map((r) => {
|
||||
if (r.type === "coins") return `• 🪙 +${r.amount}`;
|
||||
const info = rewardItems.get(r.itemKey!);
|
||||
const label = formatItemLabel(
|
||||
info ?? { key: r.itemKey!, name: null, icon: null }
|
||||
);
|
||||
return `• ${label} x${r.qty ?? 1}`;
|
||||
})
|
||||
.join("\n")
|
||||
: "• —";
|
||||
const mobsLines = result.mobs.length
|
||||
? result.mobs.map((m) => `• ${m}`).join("\n")
|
||||
: "• —";
|
||||
const toolInfo = result.tool?.key
|
||||
? `${formatItemLabel(
|
||||
rewardItems.get(result.tool.key) ?? {
|
||||
key: result.tool.key,
|
||||
name: null,
|
||||
icon: null,
|
||||
},
|
||||
{ fallbackIcon: "🗡️" }
|
||||
)}${
|
||||
result.tool.broken
|
||||
? " (rota)"
|
||||
: ` (-${result.tool.durabilityDelta ?? 0} dur.)`
|
||||
}`
|
||||
: "—";
|
||||
|
||||
const blocks = [textBlock("# ⚔️ Arena")];
|
||||
|
||||
if (globalNotice) {
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
@@ -87,16 +137,32 @@ export const command: CommandMessage = {
|
||||
}
|
||||
|
||||
blocks.push(dividerBlock());
|
||||
const areaScope = source === 'global' ? '🌐 Configuración global' : '📍 Configuración local';
|
||||
blocks.push(textBlock(`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Arma:** ${toolInfo}`));
|
||||
const areaScope =
|
||||
source === "global"
|
||||
? "🌐 Configuración global"
|
||||
: "📍 Configuración local";
|
||||
blocks.push(
|
||||
textBlock(
|
||||
`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Arma:** ${toolInfo}`
|
||||
)
|
||||
);
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
blocks.push(textBlock(`**Recompensas**\n${rewardLines}`));
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
blocks.push(textBlock(`**Enemigos**\n${mobsLines}`));
|
||||
|
||||
// Añadir metadata del área
|
||||
const metaBlocks = buildAreaMetadataBlocks(area);
|
||||
if (metaBlocks.length) {
|
||||
blocks.push(dividerBlock());
|
||||
blocks.push(...metaBlocks);
|
||||
}
|
||||
|
||||
if (newAchievements.length > 0) {
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 2 }));
|
||||
const achLines = newAchievements.map(ach => `✨ **${ach.name}** — ${ach.description}`).join('\n');
|
||||
const achLines = newAchievements
|
||||
.map((ach) => `✨ **${ach.name}** — ${ach.description}`)
|
||||
.join("\n");
|
||||
blocks.push(textBlock(`🏆 ¡Logro desbloqueado!\n${achLines}`));
|
||||
}
|
||||
|
||||
@@ -105,6 +171,5 @@ export const command: CommandMessage = {
|
||||
} catch (e: any) {
|
||||
await message.reply(`❌ No se pudo pelear: ${e?.message ?? e}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { runMinigame } from '../../../game/minigames/service';
|
||||
import { getDefaultLevel, findBestToolKey, parseGameArgs, resolveGuildAreaWithFallback, resolveAreaByType, sendDisplayReply, fetchItemBasics, formatItemLabel } from './_helpers';
|
||||
import { updateStats } from '../../../game/stats/service';
|
||||
import { updateQuestProgress } from '../../../game/quests/service';
|
||||
import { checkAchievements } from '../../../game/achievements/service';
|
||||
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
||||
import type { CommandMessage } from "../../../core/types/commands";
|
||||
import type Amayo from "../../../core/client";
|
||||
import { runMinigame } from "../../../game/minigames/service";
|
||||
import {
|
||||
getDefaultLevel,
|
||||
findBestToolKey,
|
||||
parseGameArgs,
|
||||
resolveGuildAreaWithFallback,
|
||||
resolveAreaByType,
|
||||
sendDisplayReply,
|
||||
fetchItemBasics,
|
||||
formatItemLabel,
|
||||
} from "./_helpers";
|
||||
import { updateStats } from "../../../game/stats/service";
|
||||
import { updateQuestProgress } from "../../../game/quests/service";
|
||||
import { checkAchievements } from "../../../game/achievements/service";
|
||||
import {
|
||||
buildDisplay,
|
||||
dividerBlock,
|
||||
textBlock,
|
||||
} from "../../../core/lib/componentsV2";
|
||||
import { buildAreaMetadataBlocks } from "./_helpers";
|
||||
|
||||
const FISHING_ACCENT = 0x1ABC9C;
|
||||
const FISHING_ACCENT = 0x1abc9c;
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'pescar',
|
||||
type: 'message',
|
||||
aliases: ['fish'],
|
||||
name: "pescar",
|
||||
type: "message",
|
||||
aliases: ["fish"],
|
||||
cooldown: 5,
|
||||
description: 'Pesca en la laguna (usa caña si está disponible) y obtén recompensas.',
|
||||
usage: 'pescar [nivel] [toolKey] [area:clave] (ej: pescar 1 tool.rod.basic)',
|
||||
description:
|
||||
"Pesca en la laguna (usa caña si está disponible) y obtén recompensas.",
|
||||
usage: "pescar [nivel] [toolKey] [area:clave] (ej: pescar 1 tool.rod.basic)",
|
||||
run: async (message, args, _client: Amayo) => {
|
||||
const userId = message.author.id;
|
||||
const guildId = message.guild!.id;
|
||||
@@ -23,55 +38,85 @@ export const command: CommandMessage = {
|
||||
|
||||
const areaInfo = areaOverride
|
||||
? await resolveGuildAreaWithFallback(guildId, areaOverride)
|
||||
: await resolveAreaByType(guildId, 'LAGOON');
|
||||
: await resolveAreaByType(guildId, "LAGOON");
|
||||
|
||||
if (!areaInfo.area) {
|
||||
if (areaOverride) {
|
||||
await message.reply(`⚠️ No existe un área con key \`${areaOverride}\` para este servidor.`);
|
||||
await message.reply(
|
||||
`⚠️ No existe un área con key \`${areaOverride}\` para este servidor.`
|
||||
);
|
||||
} else {
|
||||
await message.reply('⚠️ No hay un área de tipo **LAGOON** configurada. Crea una con `!area-crear` o especifica `area:<key>`.');
|
||||
await message.reply(
|
||||
"⚠️ No hay un área de tipo **LAGOON** configurada. Crea una con `!area-crear` o especifica `area:<key>`."
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { area, source } = areaInfo;
|
||||
const globalNotice = source === 'global'
|
||||
? `ℹ️ Usando configuración global para \`${area.key}\`. Puedes crear \`gameArea\` tipo **LAGOON** para personalizarla en este servidor.`
|
||||
: null;
|
||||
const globalNotice =
|
||||
source === "global"
|
||||
? `ℹ️ Usando configuración global para \`${area.key}\`. Puedes crear \`gameArea\` tipo **LAGOON** para personalizarla en este servidor.`
|
||||
: null;
|
||||
|
||||
const level = levelArg ?? await getDefaultLevel(userId, guildId, area.id);
|
||||
const toolKey = providedTool ?? await findBestToolKey(userId, guildId, 'rod');
|
||||
const level = levelArg ?? (await getDefaultLevel(userId, guildId, area.id));
|
||||
const toolKey =
|
||||
providedTool ?? (await findBestToolKey(userId, guildId, "rod"));
|
||||
|
||||
try {
|
||||
const result = await runMinigame(userId, guildId, area.key, level, { toolKey: toolKey ?? undefined });
|
||||
const result = await runMinigame(userId, guildId, area.key, level, {
|
||||
toolKey: toolKey ?? undefined,
|
||||
});
|
||||
|
||||
const rewardKeys = result.rewards
|
||||
.filter((r): r is { type: 'item'; itemKey: string; qty?: number } => r.type === 'item' && Boolean(r.itemKey))
|
||||
.filter(
|
||||
(r): r is { type: "item"; itemKey: string; qty?: number } =>
|
||||
r.type === "item" && Boolean(r.itemKey)
|
||||
)
|
||||
.map((r) => r.itemKey!);
|
||||
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||
|
||||
// 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');
|
||||
await updateQuestProgress(userId, guildId, "fish_count", 1);
|
||||
const newAchievements = await checkAchievements(
|
||||
userId,
|
||||
guildId,
|
||||
"fish_count"
|
||||
);
|
||||
|
||||
const rewardLines = result.rewards.length
|
||||
? result.rewards.map((r) => {
|
||||
if (r.type === 'coins') return `• 🪙 +${r.amount}`;
|
||||
const info = rewardItems.get(r.itemKey!);
|
||||
const label = formatItemLabel(info ?? { key: r.itemKey!, name: null, icon: null });
|
||||
return `• ${label} x${r.qty ?? 1}`;
|
||||
}).join('\n')
|
||||
: '• —';
|
||||
? result.rewards
|
||||
.map((r) => {
|
||||
if (r.type === "coins") return `• 🪙 +${r.amount}`;
|
||||
const info = rewardItems.get(r.itemKey!);
|
||||
const label = formatItemLabel(
|
||||
info ?? { key: r.itemKey!, name: null, icon: null }
|
||||
);
|
||||
return `• ${label} x${r.qty ?? 1}`;
|
||||
})
|
||||
.join("\n")
|
||||
: "• —";
|
||||
const mobsLines = result.mobs.length
|
||||
? result.mobs.map(m => `• ${m}`).join('\n')
|
||||
: '• —';
|
||||
? result.mobs.map((m) => `• ${m}`).join("\n")
|
||||
: "• —";
|
||||
const toolInfo = result.tool?.key
|
||||
? `${formatItemLabel(rewardItems.get(result.tool.key) ?? { key: result.tool.key, name: null, icon: null }, { fallbackIcon: '🎣' })}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta ?? 0} dur.)`}`
|
||||
: '—';
|
||||
? `${formatItemLabel(
|
||||
rewardItems.get(result.tool.key) ?? {
|
||||
key: result.tool.key,
|
||||
name: null,
|
||||
icon: null,
|
||||
},
|
||||
{ fallbackIcon: "🎣" }
|
||||
)}${
|
||||
result.tool.broken
|
||||
? " (rota)"
|
||||
: ` (-${result.tool.durabilityDelta ?? 0} dur.)`
|
||||
}`
|
||||
: "—";
|
||||
|
||||
const blocks = [textBlock('# 🎣 Pesca')];
|
||||
const blocks = [textBlock("# 🎣 Pesca")];
|
||||
|
||||
if (globalNotice) {
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
@@ -79,16 +124,32 @@ export const command: CommandMessage = {
|
||||
}
|
||||
|
||||
blocks.push(dividerBlock());
|
||||
const areaScope = source === 'global' ? '🌐 Configuración global' : '📍 Configuración local';
|
||||
blocks.push(textBlock(`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`));
|
||||
const areaScope =
|
||||
source === "global"
|
||||
? "🌐 Configuración global"
|
||||
: "📍 Configuración local";
|
||||
blocks.push(
|
||||
textBlock(
|
||||
`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`
|
||||
)
|
||||
);
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
blocks.push(textBlock(`**Recompensas**\n${rewardLines}`));
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
blocks.push(textBlock(`**Mobs**\n${mobsLines}`));
|
||||
|
||||
// Añadir metadata del área
|
||||
const metaBlocks = buildAreaMetadataBlocks(area);
|
||||
if (metaBlocks.length) {
|
||||
blocks.push(dividerBlock());
|
||||
blocks.push(...metaBlocks);
|
||||
}
|
||||
|
||||
if (newAchievements.length > 0) {
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 2 }));
|
||||
const achLines = newAchievements.map(ach => `✨ **${ach.name}** — ${ach.description}`).join('\n');
|
||||
const achLines = newAchievements
|
||||
.map((ach) => `✨ **${ach.name}** — ${ach.description}`)
|
||||
.join("\n");
|
||||
blocks.push(textBlock(`🏆 ¡Logro desbloqueado!\n${achLines}`));
|
||||
}
|
||||
|
||||
@@ -97,6 +158,5 @@ export const command: CommandMessage = {
|
||||
} catch (e: any) {
|
||||
await message.reply(`❌ No se pudo pescar: ${e?.message ?? e}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,71 +1,119 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { runMinigame } from '../../../game/minigames/service';
|
||||
import { resolveArea, getDefaultLevel, findBestToolKey, sendDisplayReply, fetchItemBasics, formatItemLabel } from './_helpers';
|
||||
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
||||
import type { CommandMessage } from "../../../core/types/commands";
|
||||
import type Amayo from "../../../core/client";
|
||||
import { runMinigame } from "../../../game/minigames/service";
|
||||
import {
|
||||
resolveArea,
|
||||
getDefaultLevel,
|
||||
findBestToolKey,
|
||||
sendDisplayReply,
|
||||
fetchItemBasics,
|
||||
formatItemLabel,
|
||||
} from "./_helpers";
|
||||
import {
|
||||
buildDisplay,
|
||||
dividerBlock,
|
||||
textBlock,
|
||||
} from "../../../core/lib/componentsV2";
|
||||
import { buildAreaMetadataBlocks } from "./_helpers";
|
||||
|
||||
const FARM_ACCENT = 0x2ECC71;
|
||||
const FARM_ACCENT = 0x2ecc71;
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'plantar',
|
||||
type: 'message',
|
||||
aliases: ['farm'],
|
||||
name: "plantar",
|
||||
type: "message",
|
||||
aliases: ["farm"],
|
||||
cooldown: 5,
|
||||
description: 'Planta/cosecha en el campo (usa azada si está disponible).',
|
||||
usage: 'plantar [nivel] [toolKey] (ej: plantar 1 tool.hoe.basic)',
|
||||
description: "Planta/cosecha en el campo (usa azada si está disponible).",
|
||||
usage: "plantar [nivel] [toolKey] (ej: plantar 1 tool.hoe.basic)",
|
||||
run: async (message, args, _client: Amayo) => {
|
||||
const userId = message.author.id;
|
||||
const guildId = message.guild!.id;
|
||||
const areaKey = 'farm.field';
|
||||
const areaKey = "farm.field";
|
||||
|
||||
const area = await resolveArea(guildId, areaKey);
|
||||
if (!area) { await message.reply('⚠️ Área de cultivo no configurada. Crea `gameArea` con key `farm.field`.'); return; }
|
||||
if (!area) {
|
||||
await message.reply(
|
||||
"⚠️ Área de cultivo no configurada. Crea `gameArea` con key `farm.field`."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const levelArg = args[0] && /^\d+$/.test(args[0]) ? parseInt(args[0], 10) : null;
|
||||
const levelArg =
|
||||
args[0] && /^\d+$/.test(args[0]) ? parseInt(args[0], 10) : null;
|
||||
const providedTool = args.find((a) => a && !/^\d+$/.test(a));
|
||||
|
||||
const level = levelArg ?? await getDefaultLevel(userId, guildId, area.id);
|
||||
const toolKey = providedTool ?? await findBestToolKey(userId, guildId, 'hoe');
|
||||
const level = levelArg ?? (await getDefaultLevel(userId, guildId, area.id));
|
||||
const toolKey =
|
||||
providedTool ?? (await findBestToolKey(userId, guildId, "hoe"));
|
||||
|
||||
try {
|
||||
const result = await runMinigame(userId, guildId, areaKey, level, { toolKey: toolKey ?? undefined });
|
||||
const result = await runMinigame(userId, guildId, areaKey, level, {
|
||||
toolKey: toolKey ?? undefined,
|
||||
});
|
||||
|
||||
const rewardKeys = result.rewards
|
||||
.filter((r): r is { type: 'item'; itemKey: string; qty?: number } => r.type === 'item' && Boolean(r.itemKey))
|
||||
.filter(
|
||||
(r): r is { type: "item"; itemKey: string; qty?: number } =>
|
||||
r.type === "item" && Boolean(r.itemKey)
|
||||
)
|
||||
.map((r) => r.itemKey!);
|
||||
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||
|
||||
const rewardLines = result.rewards.length
|
||||
? result.rewards.map((r) => {
|
||||
if (r.type === 'coins') return `• 🪙 +${r.amount}`;
|
||||
const info = rewardItems.get(r.itemKey!);
|
||||
const label = formatItemLabel(info ?? { key: r.itemKey!, name: null, icon: null });
|
||||
return `• ${label} x${r.qty ?? 1}`;
|
||||
}).join('\n')
|
||||
: '• —';
|
||||
? result.rewards
|
||||
.map((r) => {
|
||||
if (r.type === "coins") return `• 🪙 +${r.amount}`;
|
||||
const info = rewardItems.get(r.itemKey!);
|
||||
const label = formatItemLabel(
|
||||
info ?? { key: r.itemKey!, name: null, icon: null }
|
||||
);
|
||||
return `• ${label} x${r.qty ?? 1}`;
|
||||
})
|
||||
.join("\n")
|
||||
: "• —";
|
||||
const mobsLines = result.mobs.length
|
||||
? result.mobs.map(m => `• ${m}`).join('\n')
|
||||
: '• —';
|
||||
? result.mobs.map((m) => `• ${m}`).join("\n")
|
||||
: "• —";
|
||||
const toolInfo = result.tool?.key
|
||||
? `${formatItemLabel(rewardItems.get(result.tool.key) ?? { key: result.tool.key, name: null, icon: null }, { fallbackIcon: '🪓' })}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta ?? 0} dur.)`}`
|
||||
: '—';
|
||||
? `${formatItemLabel(
|
||||
rewardItems.get(result.tool.key) ?? {
|
||||
key: result.tool.key,
|
||||
name: null,
|
||||
icon: null,
|
||||
},
|
||||
{ fallbackIcon: "🪓" }
|
||||
)}${
|
||||
result.tool.broken
|
||||
? " (rota)"
|
||||
: ` (-${result.tool.durabilityDelta ?? 0} dur.)`
|
||||
}`
|
||||
: "—";
|
||||
|
||||
const blocks = [
|
||||
textBlock('# 🌱 Campo'),
|
||||
textBlock("# 🌱 Campo"),
|
||||
dividerBlock(),
|
||||
textBlock(`**Área:** \`${area.key}\`\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`),
|
||||
textBlock(
|
||||
`**Área:** \`${area.key}\`\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`
|
||||
),
|
||||
dividerBlock({ divider: false, spacing: 1 }),
|
||||
textBlock(`**Recompensas**\n${rewardLines}`),
|
||||
dividerBlock({ divider: false, spacing: 1 }),
|
||||
textBlock(`**Eventos**\n${mobsLines}`),
|
||||
];
|
||||
|
||||
// Añadir metadata del área
|
||||
const metaBlocks = buildAreaMetadataBlocks(area);
|
||||
if (metaBlocks.length) {
|
||||
blocks.push(dividerBlock());
|
||||
// @ts-ignore: extended block type allowed at runtime
|
||||
blocks.push(...metaBlocks);
|
||||
}
|
||||
|
||||
const display = buildDisplay(FARM_ACCENT, blocks);
|
||||
await sendDisplayReply(message, display);
|
||||
} catch (e: any) {
|
||||
await message.reply(`❌ No se pudo plantar: ${e?.message ?? e}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Simple Appwrite client wrapper
|
||||
// @ts-ignore
|
||||
import { Client, Databases } from "node-appwrite";
|
||||
import { Client, Databases, Storage } from "node-appwrite";
|
||||
|
||||
const endpoint = process.env.APPWRITE_ENDPOINT || "";
|
||||
const projectId = process.env.APPWRITE_PROJECT_ID || "";
|
||||
@@ -14,8 +14,21 @@ export const APPWRITE_COLLECTION_AI_CONVERSATIONS_ID =
|
||||
export const APPWRITE_COLLECTION_GUILD_CACHE_ID =
|
||||
process.env.APPWRITE_COLLECTION_GUILD_CACHE_ID || "";
|
||||
|
||||
// Optional: collections for game realtime mirrors
|
||||
export const APPWRITE_COLLECTION_QUESTS_ID =
|
||||
process.env.APPWRITE_COLLECTION_QUESTS_ID || "";
|
||||
export const APPWRITE_COLLECTION_QUEST_PROGRESS_ID =
|
||||
process.env.APPWRITE_COLLECTION_QUEST_PROGRESS_ID || "";
|
||||
export const APPWRITE_COLLECTION_SCHEDULED_ATTACKS_ID =
|
||||
process.env.APPWRITE_COLLECTION_SCHEDULED_ATTACKS_ID || "";
|
||||
|
||||
// Optional: bucket for images (areas/levels)
|
||||
export const APPWRITE_BUCKET_IMAGES_ID =
|
||||
process.env.APPWRITE_BUCKET_IMAGES_ID || "";
|
||||
|
||||
let client: Client | null = null;
|
||||
let databases: Databases | null = null;
|
||||
let storage: Storage | null = null;
|
||||
|
||||
function ensureClient() {
|
||||
if (!endpoint || !projectId || !apiKey) return null;
|
||||
@@ -25,6 +38,7 @@ function ensureClient() {
|
||||
.setProject(projectId)
|
||||
.setKey(apiKey);
|
||||
databases = new Databases(client);
|
||||
storage = new Storage(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -32,6 +46,10 @@ export function getDatabases(): Databases | null {
|
||||
return ensureClient() ? (databases as Databases) : null;
|
||||
}
|
||||
|
||||
export function getStorage(): Storage | null {
|
||||
return ensureClient() ? (storage as Storage) : null;
|
||||
}
|
||||
|
||||
export function isAppwriteConfigured(): boolean {
|
||||
return Boolean(
|
||||
endpoint &&
|
||||
@@ -61,3 +79,20 @@ export function isGuildCacheConfigured(): boolean {
|
||||
APPWRITE_COLLECTION_GUILD_CACHE_ID
|
||||
);
|
||||
}
|
||||
|
||||
export function isAppwriteStorageConfigured(): boolean {
|
||||
return Boolean(endpoint && projectId && apiKey && APPWRITE_BUCKET_IMAGES_ID);
|
||||
}
|
||||
|
||||
export function isGameRealtimeConfigured(): boolean {
|
||||
// minimal check for quests/progress and scheduled attacks mirrors
|
||||
return Boolean(
|
||||
endpoint &&
|
||||
projectId &&
|
||||
apiKey &&
|
||||
APPWRITE_DATABASE_ID &&
|
||||
(APPWRITE_COLLECTION_QUESTS_ID ||
|
||||
APPWRITE_COLLECTION_QUEST_PROGRESS_ID ||
|
||||
APPWRITE_COLLECTION_SCHEDULED_ATTACKS_ID)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
export type DisplayBlock =
|
||||
| { kind: 'text'; content: string }
|
||||
| { kind: 'divider'; divider?: boolean; spacing?: number };
|
||||
| { kind: "text"; content: string }
|
||||
| { kind: "divider"; divider?: boolean; spacing?: number }
|
||||
| { kind: "image"; url: string };
|
||||
|
||||
export const textBlock = (content: string): DisplayBlock => ({ kind: 'text', content });
|
||||
export const textBlock = (content: string): DisplayBlock => ({
|
||||
kind: "text",
|
||||
content,
|
||||
});
|
||||
|
||||
export const dividerBlock = (options: { divider?: boolean; spacing?: number } = {}): DisplayBlock => ({
|
||||
kind: 'divider',
|
||||
export const dividerBlock = (
|
||||
options: { divider?: boolean; spacing?: number } = {}
|
||||
): DisplayBlock => ({
|
||||
kind: "divider",
|
||||
divider: options.divider,
|
||||
spacing: options.spacing,
|
||||
});
|
||||
@@ -15,10 +21,15 @@ export function buildDisplay(accentColor: number, blocks: DisplayBlock[]) {
|
||||
type: 17 as const,
|
||||
accent_color: accentColor,
|
||||
components: blocks.map((block) => {
|
||||
if (block.kind === 'text') {
|
||||
if (block.kind === "text") {
|
||||
return { type: 10 as const, content: block.content };
|
||||
}
|
||||
|
||||
if (block.kind === "image") {
|
||||
// This component type will be translated by the renderer to an embed image
|
||||
return { type: 12 as const, url: block.url } as any;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 14 as const,
|
||||
divider: block.divider ?? true,
|
||||
|
||||
Reference in New Issue
Block a user