diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5424ac8..5614cec 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,38 +1,61 @@ -# Custom instructions for GitHub Copilot (discord.js 15.0.0-dev) +# 🧠 Custom Instructions for GitHub Copilot +**Project Context: Discord Bot + Game System Integration (discord.js 15.0.0-dev.1759363313-f510b5ffa)** -When generating or modifying code for Discord.js version `15.0.0-dev.1759363313-f510b5ffa`: +--- -1. **Primary Source of Truth** - - Do not assume the official docs, guide, or GitHub repositories are fully up-to-date. - - Always prioritize the installed package in `node_modules/discord.js` as the most reliable source. - - Always cross-reference with the provided file `example.ts.txt` to understand new or experimental APIs. +## 1. Primary Source of Truth +- Always treat the **installed package** in `node_modules/discord.js` as the *definitive source* of API behavior and typings. +- Do **not** rely solely on public documentation or examples; cross-verify any methods, classes, or type names directly from: + - The source code and declaration files inside `node_modules/discord.js` + - The project’s internal reference file `example.ts.txt` -2. **Secondary Sources** - - Only use these as references when the information is still valid: - - [Discord.js Guide](https://discordjs.guide) - - [discord.js GitHub](https://github.com/discordjs/discord.js) - - [Discord API Types GitHub](https://github.com/discordjs/discord-api-types) - - [Discord Developer Documentation](https://discord.com/developers/docs/intro) - - Mark clearly if the information is outdated compared to the development version. +> If discrepancies exist, assume `example.ts.txt` and local types reflect the *intended experimental API* for this build. -3. **Code Analysis** - - Inspect the type definitions and source files inside `node_modules/discord.js` before suggesting new methods or classes. - - If `example.ts.txt` shows usage not covered in documentation, assume that is the intended API for this development version. +--- -4. **Validation** - - Always suggest running: - ```bash - npx tsc --noEmit - ``` - to validate typings. - - Remind to check runtime memory and CPU usage with: - ```js - console.log(process.memoryUsage()); - ``` - and external profilers if needed. +## 2. Secondary Sources +Use these **only when confirmed to still be valid** for the current development version: +- [Discord.js Guide](https://discordjs.guide) +- [discord.js GitHub Repository](https://github.com/discordjs/discord.js) +- [Discord API Types GitHub](https://github.com/discordjs/discord-api-types) +- [Discord Developer Documentation](https://discord.com/developers/docs/intro) -5. **Communication** - - When suggesting code, state explicitly whether it comes from: - - `node_modules` (preferred, authoritative) - - `example.ts.txt` (author-provided experimental reference) - - official docs (secondary, possibly outdated) +> ⚠️ Mark explicitly when a snippet or concept originates from official docs and may be outdated. + +--- + +## 3. Code & Type Analysis Scope +Copilot must **investigate, interpret, and reference** the following project directories for all game logic and command definitions: + +- `src/game/**` +- `src/commands/game/**` +- `src/commands/admin/**` + +### Tasks: +- Analyze **all game-related classes, interfaces, and types**, including metadata structures (e.g., `GameAreaLevel`, `GameArea`, `ScheduledMobAttack`, and mission types). +- Identify how these interact with command creation and execution flows. +- Detect **missing type declarations**, inconsistent imports, or unreferenced type usages. +- Evaluate whether metadata in `GameAreaLevel` can safely include additional properties (e.g., `image`, `referenceImage`, or similar) for visual mapping of game areas. +- Verify that all related commands and editors properly support or update those fields. + +--- + +## 4. Appwrite Integration Considerations +While analyzing the above directories, also check for: +- Possible migration paths for mission and attack scheduling logic (`ScheduledMobAttack`, mission trackers) into **Appwrite Functions** or **Appwrite Realtime** for better live synchronization and event-driven execution. +- Type definitions or data structures that may need adaptation for Appwrite’s SDK. + +--- + +## 5. Validation +Before finalizing any generated code or type updates: +- Run TypeScript validation to ensure type correctness: + ```bash + npx tsc --noEmit +``` + +## 6. Communication Protocol +When Copilot suggests or modifies code, it must explicitly indicate the origin of the reference: +- 🟩 node_modules → Authoritative source (preferred) +- 🟦 example.ts.txt → Experimental / confirmed local reference +- 🟨 Official docs → Secondary, possibly outdated source diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3a6ec43..ad6e82a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,6 +12,22 @@ "--", "--noEmit" ] + }, + { + "label": "Typecheck: tsc --noEmit", + "type": "shell", + "command": "npm", + "args": [ + "run", + "-s", + "tsc", + "--", + "--noEmit" + ], + "problemMatcher": [ + "$tsc" + ], + "group": "build" } ] } \ No newline at end of file diff --git a/src/commands/messages/game/_helpers.ts b/src/commands/messages/game/_helpers.ts index ac1c978..f85e587 100644 --- a/src/commands/messages/game/_helpers.ts +++ b/src/commands/messages/game/_helpers.ts @@ -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 +) { + 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; diff --git a/src/commands/messages/game/mina.ts b/src/commands/messages/game/mina.ts index 0fa0b33..1a9d777 100644 --- a/src/commands/messages/game/mina.ts +++ b/src/commands/messages/game/mina.ts @@ -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:`.'); + await message.reply( + "⚠️ No hay un área de tipo **MINE** configurada. Crea una con `!area-crear` o especifica `area:`." + ); } 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}`); } - } + }, }; - diff --git a/src/commands/messages/game/pelear.ts b/src/commands/messages/game/pelear.ts index b3afe2c..4b6d04e 100644 --- a/src/commands/messages/game/pelear.ts +++ b/src/commands/messages/game/pelear.ts @@ -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:`.'); + await message.reply( + "⚠️ No hay un área de tipo **FIGHT** configurada. Crea una con `!area-crear` o especifica `area:`." + ); } 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}`); } - } + }, }; - diff --git a/src/commands/messages/game/pescar.ts b/src/commands/messages/game/pescar.ts index bacdd10..6b8e7c7 100644 --- a/src/commands/messages/game/pescar.ts +++ b/src/commands/messages/game/pescar.ts @@ -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:`.'); + await message.reply( + "⚠️ No hay un área de tipo **LAGOON** configurada. Crea una con `!area-crear` o especifica `area:`." + ); } 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}`); } - } + }, }; - diff --git a/src/commands/messages/game/plantar.ts b/src/commands/messages/game/plantar.ts index 915752c..6eafac1 100644 --- a/src/commands/messages/game/plantar.ts +++ b/src/commands/messages/game/plantar.ts @@ -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}`); } - } + }, }; - diff --git a/src/core/api/appwrite.ts b/src/core/api/appwrite.ts index 759b1ef..89f70eb 100644 --- a/src/core/api/appwrite.ts +++ b/src/core/api/appwrite.ts @@ -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) + ); +} diff --git a/src/core/lib/componentsV2.ts b/src/core/lib/componentsV2.ts index 0d7c7ea..4e5bb9f 100644 --- a/src/core/lib/componentsV2.ts +++ b/src/core/lib/componentsV2.ts @@ -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,