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:
87
.github/copilot-instructions.md
vendored
87
.github/copilot-instructions.md
vendored
@@ -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**
|
## 1. Primary Source of Truth
|
||||||
- Do not assume the official docs, guide, or GitHub repositories are fully up-to-date.
|
- Always treat the **installed package** in `node_modules/discord.js` as the *definitive source* of API behavior and typings.
|
||||||
- Always prioritize the installed package in `node_modules/discord.js` as the most reliable source.
|
- Do **not** rely solely on public documentation or examples; cross-verify any methods, classes, or type names directly from:
|
||||||
- Always cross-reference with the provided file `example.ts.txt` to understand new or experimental APIs.
|
- The source code and declaration files inside `node_modules/discord.js`
|
||||||
|
- The project’s internal reference file `example.ts.txt`
|
||||||
|
|
||||||
2. **Secondary Sources**
|
> If discrepancies exist, assume `example.ts.txt` and local types reflect the *intended experimental API* for this build.
|
||||||
- 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.
|
|
||||||
|
|
||||||
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**
|
## 2. Secondary Sources
|
||||||
- Always suggest running:
|
Use these **only when confirmed to still be valid** for the current development version:
|
||||||
```bash
|
- [Discord.js Guide](https://discordjs.guide)
|
||||||
npx tsc --noEmit
|
- [discord.js GitHub Repository](https://github.com/discordjs/discord.js)
|
||||||
```
|
- [Discord API Types GitHub](https://github.com/discordjs/discord-api-types)
|
||||||
to validate typings.
|
- [Discord Developer Documentation](https://discord.com/developers/docs/intro)
|
||||||
- Remind to check runtime memory and CPU usage with:
|
|
||||||
```js
|
|
||||||
console.log(process.memoryUsage());
|
|
||||||
```
|
|
||||||
and external profilers if needed.
|
|
||||||
|
|
||||||
5. **Communication**
|
> ⚠️ Mark explicitly when a snippet or concept originates from official docs and may be outdated.
|
||||||
- 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)
|
## 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
|
||||||
|
|||||||
16
.vscode/tasks.json
vendored
16
.vscode/tasks.json
vendored
@@ -12,6 +12,22 @@
|
|||||||
"--",
|
"--",
|
||||||
"--noEmit"
|
"--noEmit"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Typecheck: tsc --noEmit",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "npm",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"-s",
|
||||||
|
"tsc",
|
||||||
|
"--",
|
||||||
|
"--noEmit"
|
||||||
|
],
|
||||||
|
"problemMatcher": [
|
||||||
|
"$tsc"
|
||||||
|
],
|
||||||
|
"group": "build"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { prisma } from "../../../core/database/prisma";
|
import { prisma } from "../../../core/database/prisma";
|
||||||
|
import { textBlock, dividerBlock } from "../../../core/lib/componentsV2";
|
||||||
import type { GameArea } from "@prisma/client";
|
import type { GameArea } from "@prisma/client";
|
||||||
import type { ItemProps } from "../../../game/economy/types";
|
import type { ItemProps } from "../../../game/economy/types";
|
||||||
import type {
|
import type {
|
||||||
@@ -209,6 +210,44 @@ export async function fetchItemBasics(
|
|||||||
return result;
|
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 {
|
export interface KeyPickerOption {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -1,21 +1,36 @@
|
|||||||
import type { CommandMessage } from '../../../core/types/commands';
|
import type { CommandMessage } from "../../../core/types/commands";
|
||||||
import type Amayo from '../../../core/client';
|
import type Amayo from "../../../core/client";
|
||||||
import { runMinigame } from '../../../game/minigames/service';
|
import { runMinigame } from "../../../game/minigames/service";
|
||||||
import { getDefaultLevel, findBestToolKey, parseGameArgs, resolveGuildAreaWithFallback, resolveAreaByType, sendDisplayReply, fetchItemBasics, formatItemLabel } from './_helpers';
|
import {
|
||||||
import { updateStats } from '../../../game/stats/service';
|
getDefaultLevel,
|
||||||
import { updateQuestProgress } from '../../../game/quests/service';
|
findBestToolKey,
|
||||||
import { checkAchievements } from '../../../game/achievements/service';
|
parseGameArgs,
|
||||||
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
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 = {
|
export const command: CommandMessage = {
|
||||||
name: 'mina',
|
name: "mina",
|
||||||
type: 'message',
|
type: "message",
|
||||||
aliases: ['minar'],
|
aliases: ["minar"],
|
||||||
cooldown: 5,
|
cooldown: 5,
|
||||||
description: 'Ir a la mina (usa pico si está disponible) y obtener recompensas según el nivel.',
|
description:
|
||||||
usage: 'mina [nivel] [toolKey] [area:clave] (ej: mina 2 tool.pickaxe.basic)',
|
"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) => {
|
run: async (message, args, _client: Amayo) => {
|
||||||
const userId = message.author.id;
|
const userId = message.author.id;
|
||||||
const guildId = message.guild!.id;
|
const guildId = message.guild!.id;
|
||||||
@@ -23,59 +38,89 @@ export const command: CommandMessage = {
|
|||||||
|
|
||||||
const areaInfo = areaOverride
|
const areaInfo = areaOverride
|
||||||
? await resolveGuildAreaWithFallback(guildId, areaOverride)
|
? await resolveGuildAreaWithFallback(guildId, areaOverride)
|
||||||
: await resolveAreaByType(guildId, 'MINE');
|
: await resolveAreaByType(guildId, "MINE");
|
||||||
|
|
||||||
if (!areaInfo.area) {
|
if (!areaInfo.area) {
|
||||||
if (areaOverride) {
|
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 {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { area, source } = areaInfo;
|
const { area, source } = areaInfo;
|
||||||
const globalNotice = source === 'global'
|
const globalNotice =
|
||||||
? `ℹ️ Usando configuración global para \`${area.key}\`. Puedes crear \`gameArea\` tipo **MINE** para personalizarla en este servidor.`
|
source === "global"
|
||||||
: null;
|
? `ℹ️ 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 level = levelArg ?? (await getDefaultLevel(userId, guildId, area.id));
|
||||||
const toolKey = providedTool ?? await findBestToolKey(userId, guildId, 'pickaxe');
|
const toolKey =
|
||||||
|
providedTool ?? (await findBestToolKey(userId, guildId, "pickaxe"));
|
||||||
|
|
||||||
try {
|
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
|
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!);
|
.map((r) => r.itemKey!);
|
||||||
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||||
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||||
|
|
||||||
// Actualizar stats
|
// Actualizar stats
|
||||||
await updateStats(userId, guildId, { minesCompleted: 1 });
|
await updateStats(userId, guildId, { minesCompleted: 1 });
|
||||||
|
|
||||||
// Actualizar progreso de misiones
|
// Actualizar progreso de misiones
|
||||||
await updateQuestProgress(userId, guildId, 'mine_count', 1);
|
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.)`}`
|
|
||||||
: '—';
|
|
||||||
|
|
||||||
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) {
|
if (globalNotice) {
|
||||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
@@ -83,16 +128,32 @@ export const command: CommandMessage = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
blocks.push(dividerBlock());
|
blocks.push(dividerBlock());
|
||||||
const areaScope = source === 'global' ? '🌐 Configuración global' : '📍 Configuración local';
|
const areaScope =
|
||||||
blocks.push(textBlock(`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`));
|
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(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
blocks.push(textBlock(`**Recompensas**\n${rewardLines}`));
|
blocks.push(textBlock(`**Recompensas**\n${rewardLines}`));
|
||||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
blocks.push(textBlock(`**Mobs**\n${mobsLines}`));
|
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) {
|
if (newAchievements.length > 0) {
|
||||||
blocks.push(dividerBlock({ divider: false, spacing: 2 }));
|
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}`));
|
blocks.push(textBlock(`🏆 ¡Logro desbloqueado!\n${achLines}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +162,5 @@ export const command: CommandMessage = {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
await message.reply(`❌ No se pudo minar: ${e?.message ?? e}`);
|
await message.reply(`❌ No se pudo minar: ${e?.message ?? e}`);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,36 @@
|
|||||||
import type { CommandMessage } from '../../../core/types/commands';
|
import type { CommandMessage } from "../../../core/types/commands";
|
||||||
import type Amayo from '../../../core/client';
|
import type Amayo from "../../../core/client";
|
||||||
import { runMinigame } from '../../../game/minigames/service';
|
import { runMinigame } from "../../../game/minigames/service";
|
||||||
import { getDefaultLevel, findBestToolKey, parseGameArgs, resolveGuildAreaWithFallback, resolveAreaByType, sendDisplayReply, fetchItemBasics, formatItemLabel } from './_helpers';
|
import {
|
||||||
import { updateStats } from '../../../game/stats/service';
|
getDefaultLevel,
|
||||||
import { updateQuestProgress } from '../../../game/quests/service';
|
findBestToolKey,
|
||||||
import { checkAchievements } from '../../../game/achievements/service';
|
parseGameArgs,
|
||||||
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
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 = {
|
export const command: CommandMessage = {
|
||||||
name: 'pelear',
|
name: "pelear",
|
||||||
type: 'message',
|
type: "message",
|
||||||
aliases: ['fight','arena'],
|
aliases: ["fight", "arena"],
|
||||||
cooldown: 8,
|
cooldown: 8,
|
||||||
description: 'Entra a la arena y pelea (usa espada si está disponible).',
|
description: "Entra a la arena y pelea (usa espada si está disponible).",
|
||||||
usage: 'pelear [nivel] [toolKey] [area:clave] (ej: pelear 1 weapon.sword.iron)',
|
usage:
|
||||||
|
"pelear [nivel] [toolKey] [area:clave] (ej: pelear 1 weapon.sword.iron)",
|
||||||
run: async (message, args, _client: Amayo) => {
|
run: async (message, args, _client: Amayo) => {
|
||||||
const userId = message.author.id;
|
const userId = message.author.id;
|
||||||
const guildId = message.guild!.id;
|
const guildId = message.guild!.id;
|
||||||
@@ -23,63 +38,98 @@ export const command: CommandMessage = {
|
|||||||
|
|
||||||
const areaInfo = areaOverride
|
const areaInfo = areaOverride
|
||||||
? await resolveGuildAreaWithFallback(guildId, areaOverride)
|
? await resolveGuildAreaWithFallback(guildId, areaOverride)
|
||||||
: await resolveAreaByType(guildId, 'FIGHT');
|
: await resolveAreaByType(guildId, "FIGHT");
|
||||||
|
|
||||||
if (!areaInfo.area) {
|
if (!areaInfo.area) {
|
||||||
if (areaOverride) {
|
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 {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { area, source } = areaInfo;
|
const { area, source } = areaInfo;
|
||||||
const globalNotice = source === 'global'
|
const globalNotice =
|
||||||
? `ℹ️ Usando configuración global para \`${area.key}\`. Puedes crear \`gameArea\` tipo **FIGHT** para personalizarla en este servidor.`
|
source === "global"
|
||||||
: null;
|
? `ℹ️ 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 level = levelArg ?? (await getDefaultLevel(userId, guildId, area.id));
|
||||||
const toolKey = providedTool ?? await findBestToolKey(userId, guildId, 'sword');
|
const toolKey =
|
||||||
|
providedTool ?? (await findBestToolKey(userId, guildId, "sword"));
|
||||||
|
|
||||||
try {
|
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
|
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!);
|
.map((r) => r.itemKey!);
|
||||||
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||||
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||||
|
|
||||||
// Actualizar stats y misiones
|
// Actualizar stats y misiones
|
||||||
await updateStats(userId, guildId, { fightsCompleted: 1 });
|
await updateStats(userId, guildId, { fightsCompleted: 1 });
|
||||||
await updateQuestProgress(userId, guildId, 'fight_count', 1);
|
await updateQuestProgress(userId, guildId, "fight_count", 1);
|
||||||
|
|
||||||
// Contar mobs derrotados
|
// Contar mobs derrotados
|
||||||
const mobsCount = result.mobs.length;
|
const mobsCount = result.mobs.length;
|
||||||
if (mobsCount > 0) {
|
if (mobsCount > 0) {
|
||||||
await updateStats(userId, guildId, { mobsDefeated: mobsCount });
|
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) {
|
if (globalNotice) {
|
||||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
@@ -87,16 +137,32 @@ export const command: CommandMessage = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
blocks.push(dividerBlock());
|
blocks.push(dividerBlock());
|
||||||
const areaScope = source === 'global' ? '🌐 Configuración global' : '📍 Configuración local';
|
const areaScope =
|
||||||
blocks.push(textBlock(`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Arma:** ${toolInfo}`));
|
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(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
blocks.push(textBlock(`**Recompensas**\n${rewardLines}`));
|
blocks.push(textBlock(`**Recompensas**\n${rewardLines}`));
|
||||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
blocks.push(textBlock(`**Enemigos**\n${mobsLines}`));
|
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) {
|
if (newAchievements.length > 0) {
|
||||||
blocks.push(dividerBlock({ divider: false, spacing: 2 }));
|
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}`));
|
blocks.push(textBlock(`🏆 ¡Logro desbloqueado!\n${achLines}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +171,5 @@ export const command: CommandMessage = {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
await message.reply(`❌ No se pudo pelear: ${e?.message ?? e}`);
|
await message.reply(`❌ No se pudo pelear: ${e?.message ?? e}`);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,36 @@
|
|||||||
import type { CommandMessage } from '../../../core/types/commands';
|
import type { CommandMessage } from "../../../core/types/commands";
|
||||||
import type Amayo from '../../../core/client';
|
import type Amayo from "../../../core/client";
|
||||||
import { runMinigame } from '../../../game/minigames/service';
|
import { runMinigame } from "../../../game/minigames/service";
|
||||||
import { getDefaultLevel, findBestToolKey, parseGameArgs, resolveGuildAreaWithFallback, resolveAreaByType, sendDisplayReply, fetchItemBasics, formatItemLabel } from './_helpers';
|
import {
|
||||||
import { updateStats } from '../../../game/stats/service';
|
getDefaultLevel,
|
||||||
import { updateQuestProgress } from '../../../game/quests/service';
|
findBestToolKey,
|
||||||
import { checkAchievements } from '../../../game/achievements/service';
|
parseGameArgs,
|
||||||
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
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 = {
|
export const command: CommandMessage = {
|
||||||
name: 'pescar',
|
name: "pescar",
|
||||||
type: 'message',
|
type: "message",
|
||||||
aliases: ['fish'],
|
aliases: ["fish"],
|
||||||
cooldown: 5,
|
cooldown: 5,
|
||||||
description: 'Pesca en la laguna (usa caña si está disponible) y obtén recompensas.',
|
description:
|
||||||
usage: 'pescar [nivel] [toolKey] [area:clave] (ej: pescar 1 tool.rod.basic)',
|
"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) => {
|
run: async (message, args, _client: Amayo) => {
|
||||||
const userId = message.author.id;
|
const userId = message.author.id;
|
||||||
const guildId = message.guild!.id;
|
const guildId = message.guild!.id;
|
||||||
@@ -23,55 +38,85 @@ export const command: CommandMessage = {
|
|||||||
|
|
||||||
const areaInfo = areaOverride
|
const areaInfo = areaOverride
|
||||||
? await resolveGuildAreaWithFallback(guildId, areaOverride)
|
? await resolveGuildAreaWithFallback(guildId, areaOverride)
|
||||||
: await resolveAreaByType(guildId, 'LAGOON');
|
: await resolveAreaByType(guildId, "LAGOON");
|
||||||
|
|
||||||
if (!areaInfo.area) {
|
if (!areaInfo.area) {
|
||||||
if (areaOverride) {
|
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 {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { area, source } = areaInfo;
|
const { area, source } = areaInfo;
|
||||||
const globalNotice = source === 'global'
|
const globalNotice =
|
||||||
? `ℹ️ Usando configuración global para \`${area.key}\`. Puedes crear \`gameArea\` tipo **LAGOON** para personalizarla en este servidor.`
|
source === "global"
|
||||||
: null;
|
? `ℹ️ 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 level = levelArg ?? (await getDefaultLevel(userId, guildId, area.id));
|
||||||
const toolKey = providedTool ?? await findBestToolKey(userId, guildId, 'rod');
|
const toolKey =
|
||||||
|
providedTool ?? (await findBestToolKey(userId, guildId, "rod"));
|
||||||
|
|
||||||
try {
|
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
|
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!);
|
.map((r) => r.itemKey!);
|
||||||
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||||
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||||
|
|
||||||
// Actualizar stats y misiones
|
// Actualizar stats y misiones
|
||||||
await updateStats(userId, guildId, { fishingCompleted: 1 });
|
await updateStats(userId, guildId, { fishingCompleted: 1 });
|
||||||
await updateQuestProgress(userId, guildId, 'fish_count', 1);
|
await updateQuestProgress(userId, guildId, "fish_count", 1);
|
||||||
const newAchievements = await checkAchievements(userId, guildId, 'fish_count');
|
const newAchievements = await checkAchievements(
|
||||||
|
userId,
|
||||||
|
guildId,
|
||||||
|
"fish_count"
|
||||||
|
);
|
||||||
|
|
||||||
const rewardLines = result.rewards.length
|
const rewardLines = result.rewards.length
|
||||||
? result.rewards.map((r) => {
|
? result.rewards
|
||||||
if (r.type === 'coins') return `• 🪙 +${r.amount}`;
|
.map((r) => {
|
||||||
const info = rewardItems.get(r.itemKey!);
|
if (r.type === "coins") return `• 🪙 +${r.amount}`;
|
||||||
const label = formatItemLabel(info ?? { key: r.itemKey!, name: null, icon: null });
|
const info = rewardItems.get(r.itemKey!);
|
||||||
return `• ${label} x${r.qty ?? 1}`;
|
const label = formatItemLabel(
|
||||||
}).join('\n')
|
info ?? { key: r.itemKey!, name: null, icon: null }
|
||||||
: '• —';
|
);
|
||||||
|
return `• ${label} x${r.qty ?? 1}`;
|
||||||
|
})
|
||||||
|
.join("\n")
|
||||||
|
: "• —";
|
||||||
const mobsLines = result.mobs.length
|
const mobsLines = result.mobs.length
|
||||||
? result.mobs.map(m => `• ${m}`).join('\n')
|
? result.mobs.map((m) => `• ${m}`).join("\n")
|
||||||
: '• —';
|
: "• —";
|
||||||
const toolInfo = result.tool?.key
|
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) {
|
if (globalNotice) {
|
||||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
@@ -79,16 +124,32 @@ export const command: CommandMessage = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
blocks.push(dividerBlock());
|
blocks.push(dividerBlock());
|
||||||
const areaScope = source === 'global' ? '🌐 Configuración global' : '📍 Configuración local';
|
const areaScope =
|
||||||
blocks.push(textBlock(`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`));
|
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(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
blocks.push(textBlock(`**Recompensas**\n${rewardLines}`));
|
blocks.push(textBlock(`**Recompensas**\n${rewardLines}`));
|
||||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
blocks.push(textBlock(`**Mobs**\n${mobsLines}`));
|
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) {
|
if (newAchievements.length > 0) {
|
||||||
blocks.push(dividerBlock({ divider: false, spacing: 2 }));
|
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}`));
|
blocks.push(textBlock(`🏆 ¡Logro desbloqueado!\n${achLines}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +158,5 @@ export const command: CommandMessage = {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
await message.reply(`❌ No se pudo pescar: ${e?.message ?? e}`);
|
await message.reply(`❌ No se pudo pescar: ${e?.message ?? e}`);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +1,119 @@
|
|||||||
import type { CommandMessage } from '../../../core/types/commands';
|
import type { CommandMessage } from "../../../core/types/commands";
|
||||||
import type Amayo from '../../../core/client';
|
import type Amayo from "../../../core/client";
|
||||||
import { runMinigame } from '../../../game/minigames/service';
|
import { runMinigame } from "../../../game/minigames/service";
|
||||||
import { resolveArea, getDefaultLevel, findBestToolKey, sendDisplayReply, fetchItemBasics, formatItemLabel } from './_helpers';
|
import {
|
||||||
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
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 = {
|
export const command: CommandMessage = {
|
||||||
name: 'plantar',
|
name: "plantar",
|
||||||
type: 'message',
|
type: "message",
|
||||||
aliases: ['farm'],
|
aliases: ["farm"],
|
||||||
cooldown: 5,
|
cooldown: 5,
|
||||||
description: 'Planta/cosecha en el campo (usa azada si está disponible).',
|
description: "Planta/cosecha en el campo (usa azada si está disponible).",
|
||||||
usage: 'plantar [nivel] [toolKey] (ej: plantar 1 tool.hoe.basic)',
|
usage: "plantar [nivel] [toolKey] (ej: plantar 1 tool.hoe.basic)",
|
||||||
run: async (message, args, _client: Amayo) => {
|
run: async (message, args, _client: Amayo) => {
|
||||||
const userId = message.author.id;
|
const userId = message.author.id;
|
||||||
const guildId = message.guild!.id;
|
const guildId = message.guild!.id;
|
||||||
const areaKey = 'farm.field';
|
const areaKey = "farm.field";
|
||||||
|
|
||||||
const area = await resolveArea(guildId, areaKey);
|
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 providedTool = args.find((a) => a && !/^\d+$/.test(a));
|
||||||
|
|
||||||
const level = levelArg ?? await getDefaultLevel(userId, guildId, area.id);
|
const level = levelArg ?? (await getDefaultLevel(userId, guildId, area.id));
|
||||||
const toolKey = providedTool ?? await findBestToolKey(userId, guildId, 'hoe');
|
const toolKey =
|
||||||
|
providedTool ?? (await findBestToolKey(userId, guildId, "hoe"));
|
||||||
|
|
||||||
try {
|
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
|
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!);
|
.map((r) => r.itemKey!);
|
||||||
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||||
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||||
|
|
||||||
const rewardLines = result.rewards.length
|
const rewardLines = result.rewards.length
|
||||||
? result.rewards.map((r) => {
|
? result.rewards
|
||||||
if (r.type === 'coins') return `• 🪙 +${r.amount}`;
|
.map((r) => {
|
||||||
const info = rewardItems.get(r.itemKey!);
|
if (r.type === "coins") return `• 🪙 +${r.amount}`;
|
||||||
const label = formatItemLabel(info ?? { key: r.itemKey!, name: null, icon: null });
|
const info = rewardItems.get(r.itemKey!);
|
||||||
return `• ${label} x${r.qty ?? 1}`;
|
const label = formatItemLabel(
|
||||||
}).join('\n')
|
info ?? { key: r.itemKey!, name: null, icon: null }
|
||||||
: '• —';
|
);
|
||||||
|
return `• ${label} x${r.qty ?? 1}`;
|
||||||
|
})
|
||||||
|
.join("\n")
|
||||||
|
: "• —";
|
||||||
const mobsLines = result.mobs.length
|
const mobsLines = result.mobs.length
|
||||||
? result.mobs.map(m => `• ${m}`).join('\n')
|
? result.mobs.map((m) => `• ${m}`).join("\n")
|
||||||
: '• —';
|
: "• —";
|
||||||
const toolInfo = result.tool?.key
|
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 = [
|
const blocks = [
|
||||||
textBlock('# 🌱 Campo'),
|
textBlock("# 🌱 Campo"),
|
||||||
dividerBlock(),
|
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 }),
|
dividerBlock({ divider: false, spacing: 1 }),
|
||||||
textBlock(`**Recompensas**\n${rewardLines}`),
|
textBlock(`**Recompensas**\n${rewardLines}`),
|
||||||
dividerBlock({ divider: false, spacing: 1 }),
|
dividerBlock({ divider: false, spacing: 1 }),
|
||||||
textBlock(`**Eventos**\n${mobsLines}`),
|
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);
|
const display = buildDisplay(FARM_ACCENT, blocks);
|
||||||
await sendDisplayReply(message, display);
|
await sendDisplayReply(message, display);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
await message.reply(`❌ No se pudo plantar: ${e?.message ?? e}`);
|
await message.reply(`❌ No se pudo plantar: ${e?.message ?? e}`);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Simple Appwrite client wrapper
|
// Simple Appwrite client wrapper
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { Client, Databases } from "node-appwrite";
|
import { Client, Databases, Storage } from "node-appwrite";
|
||||||
|
|
||||||
const endpoint = process.env.APPWRITE_ENDPOINT || "";
|
const endpoint = process.env.APPWRITE_ENDPOINT || "";
|
||||||
const projectId = process.env.APPWRITE_PROJECT_ID || "";
|
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 =
|
export const APPWRITE_COLLECTION_GUILD_CACHE_ID =
|
||||||
process.env.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 client: Client | null = null;
|
||||||
let databases: Databases | null = null;
|
let databases: Databases | null = null;
|
||||||
|
let storage: Storage | null = null;
|
||||||
|
|
||||||
function ensureClient() {
|
function ensureClient() {
|
||||||
if (!endpoint || !projectId || !apiKey) return null;
|
if (!endpoint || !projectId || !apiKey) return null;
|
||||||
@@ -25,6 +38,7 @@ function ensureClient() {
|
|||||||
.setProject(projectId)
|
.setProject(projectId)
|
||||||
.setKey(apiKey);
|
.setKey(apiKey);
|
||||||
databases = new Databases(client);
|
databases = new Databases(client);
|
||||||
|
storage = new Storage(client);
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +46,10 @@ export function getDatabases(): Databases | null {
|
|||||||
return ensureClient() ? (databases as Databases) : null;
|
return ensureClient() ? (databases as Databases) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getStorage(): Storage | null {
|
||||||
|
return ensureClient() ? (storage as Storage) : null;
|
||||||
|
}
|
||||||
|
|
||||||
export function isAppwriteConfigured(): boolean {
|
export function isAppwriteConfigured(): boolean {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
endpoint &&
|
endpoint &&
|
||||||
@@ -61,3 +79,20 @@ export function isGuildCacheConfigured(): boolean {
|
|||||||
APPWRITE_COLLECTION_GUILD_CACHE_ID
|
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 =
|
export type DisplayBlock =
|
||||||
| { kind: 'text'; content: string }
|
| { kind: "text"; content: string }
|
||||||
| { kind: 'divider'; divider?: boolean; spacing?: number };
|
| { 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 => ({
|
export const dividerBlock = (
|
||||||
kind: 'divider',
|
options: { divider?: boolean; spacing?: number } = {}
|
||||||
|
): DisplayBlock => ({
|
||||||
|
kind: "divider",
|
||||||
divider: options.divider,
|
divider: options.divider,
|
||||||
spacing: options.spacing,
|
spacing: options.spacing,
|
||||||
});
|
});
|
||||||
@@ -15,10 +21,15 @@ export function buildDisplay(accentColor: number, blocks: DisplayBlock[]) {
|
|||||||
type: 17 as const,
|
type: 17 as const,
|
||||||
accent_color: accentColor,
|
accent_color: accentColor,
|
||||||
components: blocks.map((block) => {
|
components: blocks.map((block) => {
|
||||||
if (block.kind === 'text') {
|
if (block.kind === "text") {
|
||||||
return { type: 10 as const, content: block.content };
|
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 {
|
return {
|
||||||
type: 14 as const,
|
type: 14 as const,
|
||||||
divider: block.divider ?? true,
|
divider: block.divider ?? true,
|
||||||
|
|||||||
Reference in New Issue
Block a user