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**
|
||||
- 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
|
||||
|
||||
16
.vscode/tasks.json
vendored
16
.vscode/tasks.json
vendored
@@ -12,6 +12,22 @@
|
||||
"--",
|
||||
"--noEmit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Typecheck: tsc --noEmit",
|
||||
"type": "shell",
|
||||
"command": "npm",
|
||||
"args": [
|
||||
"run",
|
||||
"-s",
|
||||
"tsc",
|
||||
"--",
|
||||
"--noEmit"
|
||||
],
|
||||
"problemMatcher": [
|
||||
"$tsc"
|
||||
],
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
89
package-lock.json
generated
89
package-lock.json
generated
@@ -16,6 +16,7 @@
|
||||
"chrono-node": "2.9.0",
|
||||
"discord-api-types": "0.38.26",
|
||||
"discord.js": "15.0.0-dev.1759363313-f510b5ffa",
|
||||
"ejs": "^3.1.10",
|
||||
"newrelic": "13.4.0",
|
||||
"node-appwrite": "19.1.0",
|
||||
"pino": "9.13.0",
|
||||
@@ -23,6 +24,7 @@
|
||||
"redis": "5.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/node": "24.3.1",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2"
|
||||
@@ -1054,6 +1056,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ejs": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz",
|
||||
"integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
||||
@@ -1188,6 +1197,12 @@
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -1214,6 +1229,12 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -1255,6 +1276,15 @@
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
@@ -1645,6 +1675,21 @@
|
||||
"fast-check": "^3.23.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ejs": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
||||
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"jake": "^10.8.5"
|
||||
},
|
||||
"bin": {
|
||||
"ejs": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
@@ -1788,6 +1833,15 @@
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/find-package-json": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/find-package-json/-/find-package-json-1.2.0.tgz",
|
||||
@@ -2187,6 +2241,23 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/jake": {
|
||||
"version": "10.9.4",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
|
||||
"integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"async": "^3.2.6",
|
||||
"filelist": "^1.0.4",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"jake": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||
@@ -2377,6 +2448,18 @@
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
@@ -2607,6 +2690,12 @@
|
||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/pino": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-9.13.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amayo",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.15",
|
||||
"description": "",
|
||||
"main": "src/main.ts",
|
||||
"scripts": {
|
||||
@@ -13,6 +13,7 @@
|
||||
"dev:optimized": "MEMORY_LOG_INTERVAL_SECONDS=300 ENABLE_MEMORY_OPTIMIZER=true NODE_OPTIONS='--expose-gc' npx tsx watch src/main.ts",
|
||||
"start:prod": "NODE_ENV=production NODE_OPTIONS=--max-old-space-size=384 npx tsx src/main.ts",
|
||||
"start:prod-optimized": "NODE_ENV=production ENABLE_MEMORY_OPTIMIZER=true NODE_OPTIONS='--max-old-space-size=512 --expose-gc' npx tsx src/main.ts",
|
||||
"tsc": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"seed:minigames": "tsx src/game/minigames/seed.ts",
|
||||
"start:optimize-relic": "NODE_ENV=production ENABLE_MEMORY_OPTIMIZER=true NEW_RELIC_APP_NAME=amayo NEW_RELIC_LICENSE_KEY=85ef833e676ed6ea726e23b3e373397dFFFFNRAL NODE_OPTIONS='--max-old-space-size=512 --expose-gc' npx tsx --experimental-loader=newrelic/esm-loader.mjs src/main.ts"
|
||||
@@ -29,6 +30,7 @@
|
||||
"chrono-node": "2.9.0",
|
||||
"discord-api-types": "0.38.26",
|
||||
"discord.js": "15.0.0-dev.1759363313-f510b5ffa",
|
||||
"ejs": "^3.1.10",
|
||||
"newrelic": "13.4.0",
|
||||
"node-appwrite": "19.1.0",
|
||||
"pino": "9.13.0",
|
||||
@@ -36,6 +38,7 @@
|
||||
"redis": "5.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/node": "24.3.1",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { prisma } from "../../../core/database/prisma";
|
||||
import { textBlock, dividerBlock } from "../../../core/lib/componentsV2";
|
||||
import type { GameArea } from "@prisma/client";
|
||||
import type { ItemProps } from "../../../game/economy/types";
|
||||
import type {
|
||||
@@ -209,6 +210,44 @@ export async function fetchItemBasics(
|
||||
return result;
|
||||
}
|
||||
|
||||
export type AreaMetadata =
|
||||
| {
|
||||
previewImage?: string;
|
||||
image?: string;
|
||||
referenceImage?: string;
|
||||
description?: string;
|
||||
[k: string]: any;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
export function buildAreaMetadataBlocks(
|
||||
area: Pick<GameArea, "metadata" | "key" | "name">
|
||||
) {
|
||||
const blocks: any[] = [];
|
||||
const meta = (area.metadata as AreaMetadata) || undefined;
|
||||
if (!meta) return blocks;
|
||||
|
||||
const img = meta.previewImage || meta.image || meta.referenceImage;
|
||||
const desc =
|
||||
typeof meta.description === "string" && meta.description.trim().length > 0
|
||||
? meta.description.trim()
|
||||
: null;
|
||||
|
||||
if (desc) {
|
||||
blocks.push(textBlock(`**🗺️ Detalles del área**\n${desc}`));
|
||||
}
|
||||
if (img && typeof img === "string") {
|
||||
// Mostrar también como texto para compatibilidad, y dejar que el renderer agregue imagen si soporta
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
blocks.push(textBlock(`**🖼️ Mapa/Imagen:** ${img}`));
|
||||
// Si el renderer soporta bloque de imagen, los consumidores podrán usarlo
|
||||
// @ts-ignore: el builder acepta bloques extendidos
|
||||
blocks.push({ kind: "image", url: img });
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
export interface KeyPickerOption {
|
||||
value: string;
|
||||
label: string;
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { hasManageGuildOrStaff } from '../../../core/lib/permissions';
|
||||
import { prisma } from '../../../core/database/prisma';
|
||||
import { Message, MessageComponentInteraction, MessageFlags, ButtonInteraction } from 'discord.js';
|
||||
import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10';
|
||||
import type { CommandMessage } from "../../../core/types/commands";
|
||||
import type Amayo from "../../../core/client";
|
||||
import { hasManageGuildOrStaff } from "../../../core/lib/permissions";
|
||||
import { prisma } from "../../../core/database/prisma";
|
||||
import {
|
||||
Message,
|
||||
MessageComponentInteraction,
|
||||
MessageFlags,
|
||||
ButtonInteraction,
|
||||
} from "discord.js";
|
||||
import {
|
||||
ComponentType,
|
||||
TextInputStyle,
|
||||
ButtonStyle,
|
||||
} from "discord-api-types/v10";
|
||||
|
||||
interface LevelState {
|
||||
areaKey: string;
|
||||
@@ -11,30 +20,52 @@ interface LevelState {
|
||||
requirements?: any;
|
||||
rewards?: any;
|
||||
mobs?: any;
|
||||
metadata?: any;
|
||||
availableFrom?: string;
|
||||
availableTo?: string;
|
||||
}
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'area-nivel',
|
||||
type: 'message',
|
||||
aliases: ['nivel-area','arealevel'],
|
||||
name: "area-nivel",
|
||||
type: "message",
|
||||
aliases: ["nivel-area", "arealevel"],
|
||||
cooldown: 10,
|
||||
description: 'Crea o edita un nivel de una GameArea (requisitos, recompensas, mobs, ventana).',
|
||||
usage: 'area-nivel <areaKey> <level>',
|
||||
description:
|
||||
"Crea o edita un nivel de una GameArea (requisitos, recompensas, mobs, ventana).",
|
||||
usage: "area-nivel <areaKey> <level>",
|
||||
run: async (message, args, _client: Amayo) => {
|
||||
const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, prisma);
|
||||
if (!allowed) { await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.'); return; }
|
||||
const allowed = await hasManageGuildOrStaff(
|
||||
message.member,
|
||||
message.guild!.id,
|
||||
prisma
|
||||
);
|
||||
if (!allowed) {
|
||||
await message.reply(
|
||||
"❌ No tienes permisos de ManageGuild ni rol de staff."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const areaKey = args[0]?.trim();
|
||||
const levelNum = parseInt(args[1] || '', 10);
|
||||
if (!areaKey || !Number.isFinite(levelNum) || levelNum <= 0) { await message.reply('Uso: `!area-nivel <areaKey> <level>`'); return; }
|
||||
const levelNum = parseInt(args[1] || "", 10);
|
||||
if (!areaKey || !Number.isFinite(levelNum) || levelNum <= 0) {
|
||||
await message.reply("Uso: `!area-nivel <areaKey> <level>`");
|
||||
return;
|
||||
}
|
||||
|
||||
const guildId = message.guild!.id;
|
||||
const area = await prisma.gameArea.findFirst({ where: { key: areaKey, OR: [{ guildId }, { guildId: null }] }, orderBy: [{ guildId: 'desc' }] });
|
||||
if (!area) { await message.reply('❌ Área no encontrada.'); return; }
|
||||
const area = await prisma.gameArea.findFirst({
|
||||
where: { key: areaKey, OR: [{ guildId }, { guildId: null }] },
|
||||
orderBy: [{ guildId: "desc" }],
|
||||
});
|
||||
if (!area) {
|
||||
await message.reply("❌ Área no encontrada.");
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = await prisma.gameAreaLevel.findFirst({ where: { areaId: area.id, level: levelNum } });
|
||||
const existing = await prisma.gameAreaLevel.findFirst({
|
||||
where: { areaId: area.id, level: levelNum },
|
||||
});
|
||||
|
||||
const state: LevelState = {
|
||||
areaKey,
|
||||
@@ -42,91 +73,264 @@ export const command: CommandMessage = {
|
||||
requirements: existing?.requirements ?? {},
|
||||
rewards: existing?.rewards ?? {},
|
||||
mobs: existing?.mobs ?? {},
|
||||
availableFrom: existing?.availableFrom ? new Date(existing.availableFrom).toISOString() : '',
|
||||
availableTo: existing?.availableTo ? new Date(existing.availableTo).toISOString() : '',
|
||||
metadata: existing?.metadata ?? {},
|
||||
availableFrom: existing?.availableFrom
|
||||
? new Date(existing.availableFrom).toISOString()
|
||||
: "",
|
||||
availableTo: existing?.availableTo
|
||||
? new Date(existing.availableTo).toISOString()
|
||||
: "",
|
||||
};
|
||||
|
||||
const editorMsg = await (message.channel as any).send({
|
||||
content: `📊 Editor Nivel Área: \`${areaKey}\` nivel ${levelNum} ${existing ? '(editar)' : '(nuevo)'}`,
|
||||
content: `📊 Editor Nivel Área: \`${areaKey}\` nivel ${levelNum} ${
|
||||
existing ? "(editar)" : "(nuevo)"
|
||||
}`,
|
||||
components: [
|
||||
{ type: 1, components: [
|
||||
{ type: 2, style: ButtonStyle.Primary, label: 'Requisitos', custom_id: 'gl_req' },
|
||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Recompensas', custom_id: 'gl_rewards' },
|
||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Mobs', custom_id: 'gl_mobs' },
|
||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'gl_window' },
|
||||
] },
|
||||
{ type: 1, components: [
|
||||
{ type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'gl_save' },
|
||||
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'gl_cancel' },
|
||||
] },
|
||||
{
|
||||
type: 1,
|
||||
components: [
|
||||
{
|
||||
type: 2,
|
||||
style: ButtonStyle.Primary,
|
||||
label: "Requisitos",
|
||||
custom_id: "gl_req",
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
style: ButtonStyle.Secondary,
|
||||
label: "Recompensas",
|
||||
custom_id: "gl_rewards",
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
style: ButtonStyle.Secondary,
|
||||
label: "Mobs",
|
||||
custom_id: "gl_mobs",
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
style: ButtonStyle.Secondary,
|
||||
label: "Ventana",
|
||||
custom_id: "gl_window",
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
style: ButtonStyle.Secondary,
|
||||
label: "Meta/Imagen",
|
||||
custom_id: "gl_meta",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 1,
|
||||
components: [
|
||||
{
|
||||
type: 2,
|
||||
style: ButtonStyle.Success,
|
||||
label: "Guardar",
|
||||
custom_id: "gl_save",
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
style: ButtonStyle.Danger,
|
||||
label: "Cancelar",
|
||||
custom_id: "gl_cancel",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i: MessageComponentInteraction)=> i.user.id === message.author.id });
|
||||
collector.on('collect', async (i: MessageComponentInteraction) => {
|
||||
const collector = editorMsg.createMessageComponentCollector({
|
||||
time: 30 * 60_000,
|
||||
filter: (i: MessageComponentInteraction) =>
|
||||
i.user.id === message.author.id,
|
||||
});
|
||||
collector.on("collect", async (i: MessageComponentInteraction) => {
|
||||
try {
|
||||
if (!i.isButton()) return;
|
||||
switch (i.customId) {
|
||||
case 'gl_cancel':
|
||||
await i.deferUpdate(); await editorMsg.edit({ content: '❌ Editor de Nivel cancelado.', components: [] }); collector.stop('cancel'); return;
|
||||
case 'gl_req': await showJsonModal(i as ButtonInteraction, state, 'requirements', 'Requisitos'); return;
|
||||
case 'gl_rewards': await showJsonModal(i as ButtonInteraction, state, 'rewards', 'Recompensas'); return;
|
||||
case 'gl_mobs': await showJsonModal(i as ButtonInteraction, state, 'mobs', 'Mobs'); return;
|
||||
case 'gl_window': await showWindowModal(i as ButtonInteraction, state); return;
|
||||
case 'gl_save':
|
||||
case "gl_cancel":
|
||||
await i.deferUpdate();
|
||||
await editorMsg.edit({
|
||||
content: "❌ Editor de Nivel cancelado.",
|
||||
components: [],
|
||||
});
|
||||
collector.stop("cancel");
|
||||
return;
|
||||
case "gl_req":
|
||||
await showJsonModal(
|
||||
i as ButtonInteraction,
|
||||
state,
|
||||
"requirements",
|
||||
"Requisitos"
|
||||
);
|
||||
return;
|
||||
case "gl_rewards":
|
||||
await showJsonModal(
|
||||
i as ButtonInteraction,
|
||||
state,
|
||||
"rewards",
|
||||
"Recompensas"
|
||||
);
|
||||
return;
|
||||
case "gl_mobs":
|
||||
await showJsonModal(i as ButtonInteraction, state, "mobs", "Mobs");
|
||||
return;
|
||||
case "gl_window":
|
||||
await showWindowModal(i as ButtonInteraction, state);
|
||||
return;
|
||||
case "gl_meta":
|
||||
await showJsonModal(
|
||||
i as ButtonInteraction,
|
||||
state,
|
||||
"metadata",
|
||||
"Metadata (incl. imagen)"
|
||||
);
|
||||
return;
|
||||
case "gl_save":
|
||||
const data = {
|
||||
areaId: area.id,
|
||||
level: state.level,
|
||||
requirements: state.requirements ?? {},
|
||||
rewards: state.rewards ?? {},
|
||||
mobs: state.mobs ?? {},
|
||||
availableFrom: state.availableFrom ? new Date(state.availableFrom) : null,
|
||||
availableTo: state.availableTo ? new Date(state.availableTo) : null,
|
||||
metadata: state.metadata ?? {},
|
||||
availableFrom: state.availableFrom
|
||||
? new Date(state.availableFrom)
|
||||
: null,
|
||||
availableTo: state.availableTo
|
||||
? new Date(state.availableTo)
|
||||
: null,
|
||||
} as const;
|
||||
if (existing) {
|
||||
await prisma.gameAreaLevel.update({ where: { id: existing.id }, data });
|
||||
await prisma.gameAreaLevel.update({
|
||||
where: { id: existing.id },
|
||||
data,
|
||||
});
|
||||
} else {
|
||||
await prisma.gameAreaLevel.create({ data });
|
||||
}
|
||||
await i.reply({ content: '✅ Nivel guardado.', flags: MessageFlags.Ephemeral });
|
||||
await editorMsg.edit({ content: `✅ Nivel guardado para \`${areaKey}\` (${state.level}).`, components: [] });
|
||||
collector.stop('saved');
|
||||
await i.reply({
|
||||
content: "✅ Nivel guardado.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
await editorMsg.edit({
|
||||
content: `✅ Nivel guardado para \`${areaKey}\` (${state.level}).`,
|
||||
components: [],
|
||||
});
|
||||
collector.stop("saved");
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral });
|
||||
if (!i.deferred && !i.replied)
|
||||
await i.reply({
|
||||
content: "❌ Error procesando la acción.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
});
|
||||
collector.on('end', async (_c: any,r: string)=> { if (r==='time') { try { await editorMsg.edit({ content:'⏰ Editor expirado.', components: [] }); } catch {} } });
|
||||
}
|
||||
collector.on("end", async (_c: any, r: string) => {
|
||||
if (r === "time") {
|
||||
try {
|
||||
await editorMsg.edit({
|
||||
content: "⏰ Editor expirado.",
|
||||
components: [],
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
async function showJsonModal(i: ButtonInteraction, state: LevelState, field: 'requirements'|'rewards'|'mobs', title: string) {
|
||||
async function showJsonModal(
|
||||
i: ButtonInteraction,
|
||||
state: LevelState,
|
||||
field: "requirements" | "rewards" | "mobs" | "metadata",
|
||||
title: string
|
||||
) {
|
||||
const current = JSON.stringify(state[field] ?? {});
|
||||
const modal = { title, customId: `gl_json_${field}`, components: [
|
||||
{ type: ComponentType.Label, label: 'JSON', component: { type: ComponentType.TextInput, customId: 'json', style: TextInputStyle.Paragraph, required: false, value: current.slice(0,4000) } },
|
||||
] } as const;
|
||||
const modal = {
|
||||
title,
|
||||
customId: `gl_json_${field}`,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Label,
|
||||
label: "JSON",
|
||||
component: {
|
||||
type: ComponentType.TextInput,
|
||||
customId: "json",
|
||||
style: TextInputStyle.Paragraph,
|
||||
required: false,
|
||||
value: current.slice(0, 4000),
|
||||
},
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
await i.showModal(modal);
|
||||
try {
|
||||
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
||||
const raw = sub.components.getTextInputValue('json');
|
||||
if (raw) { try { state[field] = JSON.parse(raw); await sub.reply({ content: '✅ Guardado.', flags: MessageFlags.Ephemeral }); } catch { await sub.reply({ content: '❌ JSON inválido.', flags: MessageFlags.Ephemeral }); } }
|
||||
else { state[field] = {}; await sub.reply({ content: 'ℹ️ Limpio.', flags: MessageFlags.Ephemeral }); }
|
||||
const raw = sub.components.getTextInputValue("json");
|
||||
if (raw) {
|
||||
try {
|
||||
state[field] = JSON.parse(raw);
|
||||
await sub.reply({
|
||||
content: "✅ Guardado.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
} catch {
|
||||
await sub.reply({
|
||||
content: "❌ JSON inválido.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
state[field] = {};
|
||||
await sub.reply({ content: "ℹ️ Limpio.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function showWindowModal(i: ButtonInteraction, state: LevelState) {
|
||||
const modal = { title: 'Ventana del Nivel', customId: 'gl_window_modal', components: [
|
||||
{ type: ComponentType.Label, label: 'Desde (ISO, opcional)', component: { type: ComponentType.TextInput, customId: 'from', style: TextInputStyle.Short, required: false, value: state.availableFrom ?? '' } },
|
||||
{ type: ComponentType.Label, label: 'Hasta (ISO, opcional)', component: { type: ComponentType.TextInput, customId: 'to', style: TextInputStyle.Short, required: false, value: state.availableTo ?? '' } },
|
||||
] } as const;
|
||||
const modal = {
|
||||
title: "Ventana del Nivel",
|
||||
customId: "gl_window_modal",
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Label,
|
||||
label: "Desde (ISO, opcional)",
|
||||
component: {
|
||||
type: ComponentType.TextInput,
|
||||
customId: "from",
|
||||
style: TextInputStyle.Short,
|
||||
required: false,
|
||||
value: state.availableFrom ?? "",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: ComponentType.Label,
|
||||
label: "Hasta (ISO, opcional)",
|
||||
component: {
|
||||
type: ComponentType.TextInput,
|
||||
customId: "to",
|
||||
style: TextInputStyle.Short,
|
||||
required: false,
|
||||
value: state.availableTo ?? "",
|
||||
},
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
await i.showModal(modal);
|
||||
try {
|
||||
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
||||
const from = sub.components.getTextInputValue('from').trim();
|
||||
const to = sub.components.getTextInputValue('to').trim();
|
||||
state.availableFrom = from || '';
|
||||
state.availableTo = to || '';
|
||||
await sub.reply({ content: '✅ Ventana actualizada.', flags: MessageFlags.Ephemeral });
|
||||
const from = sub.components.getTextInputValue("from").trim();
|
||||
const to = sub.components.getTextInputValue("to").trim();
|
||||
state.availableFrom = from || "";
|
||||
state.availableTo = to || "";
|
||||
await sub.reply({
|
||||
content: "✅ Ventana actualizada.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -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,30 +38,41 @@ export const command: CommandMessage = {
|
||||
|
||||
const areaInfo = areaOverride
|
||||
? await resolveGuildAreaWithFallback(guildId, areaOverride)
|
||||
: await resolveAreaByType(guildId, 'MINE');
|
||||
: await resolveAreaByType(guildId, "MINE");
|
||||
|
||||
if (!areaInfo.area) {
|
||||
if (areaOverride) {
|
||||
await message.reply(`⚠️ No existe un área con key \`${areaOverride}\` para este servidor.`);
|
||||
await message.reply(
|
||||
`⚠️ No existe un área con key \`${areaOverride}\` para este servidor.`
|
||||
);
|
||||
} else {
|
||||
await message.reply('⚠️ No hay un área de tipo **MINE** configurada. Crea una con `!area-crear` o especifica `area:<key>`.');
|
||||
await message.reply(
|
||||
"⚠️ No hay un área de tipo **MINE** configurada. Crea una con `!area-crear` o especifica `area:<key>`."
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { area, source } = areaInfo;
|
||||
const globalNotice = source === 'global'
|
||||
? `ℹ️ Usando configuración global para \`${area.key}\`. Puedes crear \`gameArea\` tipo **MINE** para personalizarla en este servidor.`
|
||||
: null;
|
||||
const globalNotice =
|
||||
source === "global"
|
||||
? `ℹ️ Usando configuración global para \`${area.key}\`. Puedes crear \`gameArea\` tipo **MINE** para personalizarla en este servidor.`
|
||||
: null;
|
||||
|
||||
const level = levelArg ?? await getDefaultLevel(userId, guildId, area.id);
|
||||
const toolKey = providedTool ?? await findBestToolKey(userId, guildId, 'pickaxe');
|
||||
const level = levelArg ?? (await getDefaultLevel(userId, guildId, area.id));
|
||||
const toolKey =
|
||||
providedTool ?? (await findBestToolKey(userId, guildId, "pickaxe"));
|
||||
|
||||
try {
|
||||
const result = await runMinigame(userId, guildId, area.key, level, { toolKey: toolKey ?? undefined });
|
||||
const result = await runMinigame(userId, guildId, area.key, level, {
|
||||
toolKey: toolKey ?? undefined,
|
||||
});
|
||||
|
||||
const rewardKeys = result.rewards
|
||||
.filter((r): r is { type: 'item'; itemKey: string; qty?: number } => r.type === 'item' && Boolean(r.itemKey))
|
||||
.filter(
|
||||
(r): r is { type: "item"; itemKey: string; qty?: number } =>
|
||||
r.type === "item" && Boolean(r.itemKey)
|
||||
)
|
||||
.map((r) => r.itemKey!);
|
||||
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||
@@ -55,27 +81,46 @@ export const command: CommandMessage = {
|
||||
await updateStats(userId, guildId, { minesCompleted: 1 });
|
||||
|
||||
// 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 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')
|
||||
: '• —';
|
||||
? 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('# ⛏️ Mina')];
|
||||
const blocks = [textBlock("# ⛏️ Mina")];
|
||||
|
||||
if (globalNotice) {
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
@@ -83,16 +128,32 @@ export const command: CommandMessage = {
|
||||
}
|
||||
|
||||
blocks.push(dividerBlock());
|
||||
const areaScope = source === 'global' ? '🌐 Configuración global' : '📍 Configuración local';
|
||||
blocks.push(textBlock(`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`));
|
||||
const areaScope =
|
||||
source === "global"
|
||||
? "🌐 Configuración global"
|
||||
: "📍 Configuración local";
|
||||
blocks.push(
|
||||
textBlock(
|
||||
`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`
|
||||
)
|
||||
);
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
blocks.push(textBlock(`**Recompensas**\n${rewardLines}`));
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
blocks.push(textBlock(`**Mobs**\n${mobsLines}`));
|
||||
|
||||
// Añadir metadata del área (imagen/descripcion) si existe
|
||||
const metaBlocks = buildAreaMetadataBlocks(area);
|
||||
if (metaBlocks.length) {
|
||||
blocks.push(dividerBlock());
|
||||
blocks.push(...metaBlocks);
|
||||
}
|
||||
|
||||
if (newAchievements.length > 0) {
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 2 }));
|
||||
const achLines = newAchievements.map(ach => `✨ **${ach.name}** — ${ach.description}`).join('\n');
|
||||
const achLines = newAchievements
|
||||
.map((ach) => `✨ **${ach.name}** — ${ach.description}`)
|
||||
.join("\n");
|
||||
blocks.push(textBlock(`🏆 ¡Logro desbloqueado!\n${achLines}`));
|
||||
}
|
||||
|
||||
@@ -101,6 +162,5 @@ export const command: CommandMessage = {
|
||||
} catch (e: any) {
|
||||
await message.reply(`❌ No se pudo minar: ${e?.message ?? e}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { runMinigame } from '../../../game/minigames/service';
|
||||
import { getDefaultLevel, findBestToolKey, parseGameArgs, resolveGuildAreaWithFallback, resolveAreaByType, sendDisplayReply, fetchItemBasics, formatItemLabel } from './_helpers';
|
||||
import { updateStats } from '../../../game/stats/service';
|
||||
import { updateQuestProgress } from '../../../game/quests/service';
|
||||
import { checkAchievements } from '../../../game/achievements/service';
|
||||
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
||||
import type { CommandMessage } from "../../../core/types/commands";
|
||||
import type Amayo from "../../../core/client";
|
||||
import { runMinigame } from "../../../game/minigames/service";
|
||||
import {
|
||||
getDefaultLevel,
|
||||
findBestToolKey,
|
||||
parseGameArgs,
|
||||
resolveGuildAreaWithFallback,
|
||||
resolveAreaByType,
|
||||
sendDisplayReply,
|
||||
fetchItemBasics,
|
||||
formatItemLabel,
|
||||
} from "./_helpers";
|
||||
import { updateStats } from "../../../game/stats/service";
|
||||
import { updateQuestProgress } from "../../../game/quests/service";
|
||||
import { checkAchievements } from "../../../game/achievements/service";
|
||||
import {
|
||||
buildDisplay,
|
||||
dividerBlock,
|
||||
textBlock,
|
||||
} from "../../../core/lib/componentsV2";
|
||||
import { buildAreaMetadataBlocks } from "./_helpers";
|
||||
|
||||
const FIGHT_ACCENT = 0x992D22;
|
||||
const FIGHT_ACCENT = 0x992d22;
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'pelear',
|
||||
type: 'message',
|
||||
aliases: ['fight','arena'],
|
||||
name: "pelear",
|
||||
type: "message",
|
||||
aliases: ["fight", "arena"],
|
||||
cooldown: 8,
|
||||
description: 'Entra a la arena y pelea (usa espada si está disponible).',
|
||||
usage: 'pelear [nivel] [toolKey] [area:clave] (ej: pelear 1 weapon.sword.iron)',
|
||||
description: "Entra a la arena y pelea (usa espada si está disponible).",
|
||||
usage:
|
||||
"pelear [nivel] [toolKey] [area:clave] (ej: pelear 1 weapon.sword.iron)",
|
||||
run: async (message, args, _client: Amayo) => {
|
||||
const userId = message.author.id;
|
||||
const guildId = message.guild!.id;
|
||||
@@ -23,63 +38,98 @@ export const command: CommandMessage = {
|
||||
|
||||
const areaInfo = areaOverride
|
||||
? await resolveGuildAreaWithFallback(guildId, areaOverride)
|
||||
: await resolveAreaByType(guildId, 'FIGHT');
|
||||
: await resolveAreaByType(guildId, "FIGHT");
|
||||
|
||||
if (!areaInfo.area) {
|
||||
if (areaOverride) {
|
||||
await message.reply(`⚠️ No existe un área con key \`${areaOverride}\` para este servidor.`);
|
||||
await message.reply(
|
||||
`⚠️ No existe un área con key \`${areaOverride}\` para este servidor.`
|
||||
);
|
||||
} else {
|
||||
await message.reply('⚠️ No hay un área de tipo **FIGHT** configurada. Crea una con `!area-crear` o especifica `area:<key>`.');
|
||||
await message.reply(
|
||||
"⚠️ No hay un área de tipo **FIGHT** configurada. Crea una con `!area-crear` o especifica `area:<key>`."
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { area, source } = areaInfo;
|
||||
const globalNotice = source === 'global'
|
||||
? `ℹ️ Usando configuración global para \`${area.key}\`. Puedes crear \`gameArea\` tipo **FIGHT** para personalizarla en este servidor.`
|
||||
: null;
|
||||
const globalNotice =
|
||||
source === "global"
|
||||
? `ℹ️ Usando configuración global para \`${area.key}\`. Puedes crear \`gameArea\` tipo **FIGHT** para personalizarla en este servidor.`
|
||||
: null;
|
||||
|
||||
const level = levelArg ?? await getDefaultLevel(userId, guildId, area.id);
|
||||
const toolKey = providedTool ?? await findBestToolKey(userId, guildId, 'sword');
|
||||
const level = levelArg ?? (await getDefaultLevel(userId, guildId, area.id));
|
||||
const toolKey =
|
||||
providedTool ?? (await findBestToolKey(userId, guildId, "sword"));
|
||||
|
||||
try {
|
||||
const result = await runMinigame(userId, guildId, area.key, level, { toolKey: toolKey ?? undefined });
|
||||
const result = await runMinigame(userId, guildId, area.key, level, {
|
||||
toolKey: toolKey ?? undefined,
|
||||
});
|
||||
|
||||
const rewardKeys = result.rewards
|
||||
.filter((r): r is { type: 'item'; itemKey: string; qty?: number } => r.type === 'item' && Boolean(r.itemKey))
|
||||
.filter(
|
||||
(r): r is { type: "item"; itemKey: string; qty?: number } =>
|
||||
r.type === "item" && Boolean(r.itemKey)
|
||||
)
|
||||
.map((r) => r.itemKey!);
|
||||
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||
|
||||
// Actualizar stats y misiones
|
||||
await updateStats(userId, guildId, { fightsCompleted: 1 });
|
||||
await updateQuestProgress(userId, guildId, 'fight_count', 1);
|
||||
await updateQuestProgress(userId, guildId, "fight_count", 1);
|
||||
|
||||
// Contar mobs derrotados
|
||||
const mobsCount = result.mobs.length;
|
||||
if (mobsCount > 0) {
|
||||
await updateStats(userId, guildId, { mobsDefeated: mobsCount });
|
||||
await updateQuestProgress(userId, guildId, 'mob_defeat_count', mobsCount);
|
||||
await updateQuestProgress(
|
||||
userId,
|
||||
guildId,
|
||||
"mob_defeat_count",
|
||||
mobsCount
|
||||
);
|
||||
}
|
||||
|
||||
const newAchievements = await checkAchievements(userId, guildId, 'fight_count');
|
||||
const 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')
|
||||
: '• —';
|
||||
? 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('# ⚔️ Arena')];
|
||||
const blocks = [textBlock("# ⚔️ Arena")];
|
||||
|
||||
if (globalNotice) {
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
@@ -87,16 +137,32 @@ export const command: CommandMessage = {
|
||||
}
|
||||
|
||||
blocks.push(dividerBlock());
|
||||
const areaScope = source === 'global' ? '🌐 Configuración global' : '📍 Configuración local';
|
||||
blocks.push(textBlock(`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Arma:** ${toolInfo}`));
|
||||
const areaScope =
|
||||
source === "global"
|
||||
? "🌐 Configuración global"
|
||||
: "📍 Configuración local";
|
||||
blocks.push(
|
||||
textBlock(
|
||||
`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Arma:** ${toolInfo}`
|
||||
)
|
||||
);
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
blocks.push(textBlock(`**Recompensas**\n${rewardLines}`));
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
blocks.push(textBlock(`**Enemigos**\n${mobsLines}`));
|
||||
|
||||
// Añadir metadata del área
|
||||
const metaBlocks = buildAreaMetadataBlocks(area);
|
||||
if (metaBlocks.length) {
|
||||
blocks.push(dividerBlock());
|
||||
blocks.push(...metaBlocks);
|
||||
}
|
||||
|
||||
if (newAchievements.length > 0) {
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 2 }));
|
||||
const achLines = newAchievements.map(ach => `✨ **${ach.name}** — ${ach.description}`).join('\n');
|
||||
const achLines = newAchievements
|
||||
.map((ach) => `✨ **${ach.name}** — ${ach.description}`)
|
||||
.join("\n");
|
||||
blocks.push(textBlock(`🏆 ¡Logro desbloqueado!\n${achLines}`));
|
||||
}
|
||||
|
||||
@@ -105,6 +171,5 @@ export const command: CommandMessage = {
|
||||
} catch (e: any) {
|
||||
await message.reply(`❌ No se pudo pelear: ${e?.message ?? e}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { runMinigame } from '../../../game/minigames/service';
|
||||
import { getDefaultLevel, findBestToolKey, parseGameArgs, resolveGuildAreaWithFallback, resolveAreaByType, sendDisplayReply, fetchItemBasics, formatItemLabel } from './_helpers';
|
||||
import { updateStats } from '../../../game/stats/service';
|
||||
import { updateQuestProgress } from '../../../game/quests/service';
|
||||
import { checkAchievements } from '../../../game/achievements/service';
|
||||
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
||||
import type { CommandMessage } from "../../../core/types/commands";
|
||||
import type Amayo from "../../../core/client";
|
||||
import { runMinigame } from "../../../game/minigames/service";
|
||||
import {
|
||||
getDefaultLevel,
|
||||
findBestToolKey,
|
||||
parseGameArgs,
|
||||
resolveGuildAreaWithFallback,
|
||||
resolveAreaByType,
|
||||
sendDisplayReply,
|
||||
fetchItemBasics,
|
||||
formatItemLabel,
|
||||
} from "./_helpers";
|
||||
import { updateStats } from "../../../game/stats/service";
|
||||
import { updateQuestProgress } from "../../../game/quests/service";
|
||||
import { checkAchievements } from "../../../game/achievements/service";
|
||||
import {
|
||||
buildDisplay,
|
||||
dividerBlock,
|
||||
textBlock,
|
||||
} from "../../../core/lib/componentsV2";
|
||||
import { buildAreaMetadataBlocks } from "./_helpers";
|
||||
|
||||
const FISHING_ACCENT = 0x1ABC9C;
|
||||
const FISHING_ACCENT = 0x1abc9c;
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'pescar',
|
||||
type: 'message',
|
||||
aliases: ['fish'],
|
||||
name: "pescar",
|
||||
type: "message",
|
||||
aliases: ["fish"],
|
||||
cooldown: 5,
|
||||
description: 'Pesca en la laguna (usa caña si está disponible) y obtén recompensas.',
|
||||
usage: 'pescar [nivel] [toolKey] [area:clave] (ej: pescar 1 tool.rod.basic)',
|
||||
description:
|
||||
"Pesca en la laguna (usa caña si está disponible) y obtén recompensas.",
|
||||
usage: "pescar [nivel] [toolKey] [area:clave] (ej: pescar 1 tool.rod.basic)",
|
||||
run: async (message, args, _client: Amayo) => {
|
||||
const userId = message.author.id;
|
||||
const guildId = message.guild!.id;
|
||||
@@ -23,55 +38,85 @@ export const command: CommandMessage = {
|
||||
|
||||
const areaInfo = areaOverride
|
||||
? await resolveGuildAreaWithFallback(guildId, areaOverride)
|
||||
: await resolveAreaByType(guildId, 'LAGOON');
|
||||
: await resolveAreaByType(guildId, "LAGOON");
|
||||
|
||||
if (!areaInfo.area) {
|
||||
if (areaOverride) {
|
||||
await message.reply(`⚠️ No existe un área con key \`${areaOverride}\` para este servidor.`);
|
||||
await message.reply(
|
||||
`⚠️ No existe un área con key \`${areaOverride}\` para este servidor.`
|
||||
);
|
||||
} else {
|
||||
await message.reply('⚠️ No hay un área de tipo **LAGOON** configurada. Crea una con `!area-crear` o especifica `area:<key>`.');
|
||||
await message.reply(
|
||||
"⚠️ No hay un área de tipo **LAGOON** configurada. Crea una con `!area-crear` o especifica `area:<key>`."
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { area, source } = areaInfo;
|
||||
const globalNotice = source === 'global'
|
||||
? `ℹ️ Usando configuración global para \`${area.key}\`. Puedes crear \`gameArea\` tipo **LAGOON** para personalizarla en este servidor.`
|
||||
: null;
|
||||
const globalNotice =
|
||||
source === "global"
|
||||
? `ℹ️ Usando configuración global para \`${area.key}\`. Puedes crear \`gameArea\` tipo **LAGOON** para personalizarla en este servidor.`
|
||||
: null;
|
||||
|
||||
const level = levelArg ?? await getDefaultLevel(userId, guildId, area.id);
|
||||
const toolKey = providedTool ?? await findBestToolKey(userId, guildId, 'rod');
|
||||
const level = levelArg ?? (await getDefaultLevel(userId, guildId, area.id));
|
||||
const toolKey =
|
||||
providedTool ?? (await findBestToolKey(userId, guildId, "rod"));
|
||||
|
||||
try {
|
||||
const result = await runMinigame(userId, guildId, area.key, level, { toolKey: toolKey ?? undefined });
|
||||
const result = await runMinigame(userId, guildId, area.key, level, {
|
||||
toolKey: toolKey ?? undefined,
|
||||
});
|
||||
|
||||
const rewardKeys = result.rewards
|
||||
.filter((r): r is { type: 'item'; itemKey: string; qty?: number } => r.type === 'item' && Boolean(r.itemKey))
|
||||
.filter(
|
||||
(r): r is { type: "item"; itemKey: string; qty?: number } =>
|
||||
r.type === "item" && Boolean(r.itemKey)
|
||||
)
|
||||
.map((r) => r.itemKey!);
|
||||
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||
|
||||
// Actualizar stats y misiones
|
||||
await updateStats(userId, guildId, { fishingCompleted: 1 });
|
||||
await updateQuestProgress(userId, guildId, 'fish_count', 1);
|
||||
const newAchievements = await checkAchievements(userId, guildId, 'fish_count');
|
||||
await updateQuestProgress(userId, guildId, "fish_count", 1);
|
||||
const newAchievements = await checkAchievements(
|
||||
userId,
|
||||
guildId,
|
||||
"fish_count"
|
||||
);
|
||||
|
||||
const rewardLines = result.rewards.length
|
||||
? result.rewards.map((r) => {
|
||||
if (r.type === 'coins') return `• 🪙 +${r.amount}`;
|
||||
const info = rewardItems.get(r.itemKey!);
|
||||
const label = formatItemLabel(info ?? { key: r.itemKey!, name: null, icon: null });
|
||||
return `• ${label} x${r.qty ?? 1}`;
|
||||
}).join('\n')
|
||||
: '• —';
|
||||
? result.rewards
|
||||
.map((r) => {
|
||||
if (r.type === "coins") return `• 🪙 +${r.amount}`;
|
||||
const info = rewardItems.get(r.itemKey!);
|
||||
const label = formatItemLabel(
|
||||
info ?? { key: r.itemKey!, name: null, icon: null }
|
||||
);
|
||||
return `• ${label} x${r.qty ?? 1}`;
|
||||
})
|
||||
.join("\n")
|
||||
: "• —";
|
||||
const mobsLines = result.mobs.length
|
||||
? result.mobs.map(m => `• ${m}`).join('\n')
|
||||
: '• —';
|
||||
? result.mobs.map((m) => `• ${m}`).join("\n")
|
||||
: "• —";
|
||||
const toolInfo = result.tool?.key
|
||||
? `${formatItemLabel(rewardItems.get(result.tool.key) ?? { key: result.tool.key, name: null, icon: null }, { fallbackIcon: '🎣' })}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta ?? 0} dur.)`}`
|
||||
: '—';
|
||||
? `${formatItemLabel(
|
||||
rewardItems.get(result.tool.key) ?? {
|
||||
key: result.tool.key,
|
||||
name: null,
|
||||
icon: null,
|
||||
},
|
||||
{ fallbackIcon: "🎣" }
|
||||
)}${
|
||||
result.tool.broken
|
||||
? " (rota)"
|
||||
: ` (-${result.tool.durabilityDelta ?? 0} dur.)`
|
||||
}`
|
||||
: "—";
|
||||
|
||||
const blocks = [textBlock('# 🎣 Pesca')];
|
||||
const blocks = [textBlock("# 🎣 Pesca")];
|
||||
|
||||
if (globalNotice) {
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
@@ -79,16 +124,32 @@ export const command: CommandMessage = {
|
||||
}
|
||||
|
||||
blocks.push(dividerBlock());
|
||||
const areaScope = source === 'global' ? '🌐 Configuración global' : '📍 Configuración local';
|
||||
blocks.push(textBlock(`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`));
|
||||
const areaScope =
|
||||
source === "global"
|
||||
? "🌐 Configuración global"
|
||||
: "📍 Configuración local";
|
||||
blocks.push(
|
||||
textBlock(
|
||||
`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`
|
||||
)
|
||||
);
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
blocks.push(textBlock(`**Recompensas**\n${rewardLines}`));
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
blocks.push(textBlock(`**Mobs**\n${mobsLines}`));
|
||||
|
||||
// Añadir metadata del área
|
||||
const metaBlocks = buildAreaMetadataBlocks(area);
|
||||
if (metaBlocks.length) {
|
||||
blocks.push(dividerBlock());
|
||||
blocks.push(...metaBlocks);
|
||||
}
|
||||
|
||||
if (newAchievements.length > 0) {
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 2 }));
|
||||
const achLines = newAchievements.map(ach => `✨ **${ach.name}** — ${ach.description}`).join('\n');
|
||||
const achLines = newAchievements
|
||||
.map((ach) => `✨ **${ach.name}** — ${ach.description}`)
|
||||
.join("\n");
|
||||
blocks.push(textBlock(`🏆 ¡Logro desbloqueado!\n${achLines}`));
|
||||
}
|
||||
|
||||
@@ -97,6 +158,5 @@ export const command: CommandMessage = {
|
||||
} catch (e: any) {
|
||||
await message.reply(`❌ No se pudo pescar: ${e?.message ?? e}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,71 +1,119 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { runMinigame } from '../../../game/minigames/service';
|
||||
import { resolveArea, getDefaultLevel, findBestToolKey, sendDisplayReply, fetchItemBasics, formatItemLabel } from './_helpers';
|
||||
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
||||
import type { CommandMessage } from "../../../core/types/commands";
|
||||
import type Amayo from "../../../core/client";
|
||||
import { runMinigame } from "../../../game/minigames/service";
|
||||
import {
|
||||
resolveArea,
|
||||
getDefaultLevel,
|
||||
findBestToolKey,
|
||||
sendDisplayReply,
|
||||
fetchItemBasics,
|
||||
formatItemLabel,
|
||||
} from "./_helpers";
|
||||
import {
|
||||
buildDisplay,
|
||||
dividerBlock,
|
||||
textBlock,
|
||||
} from "../../../core/lib/componentsV2";
|
||||
import { buildAreaMetadataBlocks } from "./_helpers";
|
||||
|
||||
const FARM_ACCENT = 0x2ECC71;
|
||||
const FARM_ACCENT = 0x2ecc71;
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'plantar',
|
||||
type: 'message',
|
||||
aliases: ['farm'],
|
||||
name: "plantar",
|
||||
type: "message",
|
||||
aliases: ["farm"],
|
||||
cooldown: 5,
|
||||
description: 'Planta/cosecha en el campo (usa azada si está disponible).',
|
||||
usage: 'plantar [nivel] [toolKey] (ej: plantar 1 tool.hoe.basic)',
|
||||
description: "Planta/cosecha en el campo (usa azada si está disponible).",
|
||||
usage: "plantar [nivel] [toolKey] (ej: plantar 1 tool.hoe.basic)",
|
||||
run: async (message, args, _client: Amayo) => {
|
||||
const userId = message.author.id;
|
||||
const guildId = message.guild!.id;
|
||||
const areaKey = 'farm.field';
|
||||
const areaKey = "farm.field";
|
||||
|
||||
const area = await resolveArea(guildId, areaKey);
|
||||
if (!area) { await message.reply('⚠️ Área de cultivo no configurada. Crea `gameArea` con key `farm.field`.'); return; }
|
||||
if (!area) {
|
||||
await message.reply(
|
||||
"⚠️ Área de cultivo no configurada. Crea `gameArea` con key `farm.field`."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const levelArg = args[0] && /^\d+$/.test(args[0]) ? parseInt(args[0], 10) : null;
|
||||
const levelArg =
|
||||
args[0] && /^\d+$/.test(args[0]) ? parseInt(args[0], 10) : null;
|
||||
const providedTool = args.find((a) => a && !/^\d+$/.test(a));
|
||||
|
||||
const level = levelArg ?? await getDefaultLevel(userId, guildId, area.id);
|
||||
const toolKey = providedTool ?? await findBestToolKey(userId, guildId, 'hoe');
|
||||
const level = levelArg ?? (await getDefaultLevel(userId, guildId, area.id));
|
||||
const toolKey =
|
||||
providedTool ?? (await findBestToolKey(userId, guildId, "hoe"));
|
||||
|
||||
try {
|
||||
const result = await runMinigame(userId, guildId, areaKey, level, { toolKey: toolKey ?? undefined });
|
||||
const result = await runMinigame(userId, guildId, areaKey, level, {
|
||||
toolKey: toolKey ?? undefined,
|
||||
});
|
||||
|
||||
const rewardKeys = result.rewards
|
||||
.filter((r): r is { type: 'item'; itemKey: string; qty?: number } => r.type === 'item' && Boolean(r.itemKey))
|
||||
.filter(
|
||||
(r): r is { type: "item"; itemKey: string; qty?: number } =>
|
||||
r.type === "item" && Boolean(r.itemKey)
|
||||
)
|
||||
.map((r) => r.itemKey!);
|
||||
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||
|
||||
const rewardLines = result.rewards.length
|
||||
? result.rewards.map((r) => {
|
||||
if (r.type === 'coins') return `• 🪙 +${r.amount}`;
|
||||
const info = rewardItems.get(r.itemKey!);
|
||||
const label = formatItemLabel(info ?? { key: r.itemKey!, name: null, icon: null });
|
||||
return `• ${label} x${r.qty ?? 1}`;
|
||||
}).join('\n')
|
||||
: '• —';
|
||||
? result.rewards
|
||||
.map((r) => {
|
||||
if (r.type === "coins") return `• 🪙 +${r.amount}`;
|
||||
const info = rewardItems.get(r.itemKey!);
|
||||
const label = formatItemLabel(
|
||||
info ?? { key: r.itemKey!, name: null, icon: null }
|
||||
);
|
||||
return `• ${label} x${r.qty ?? 1}`;
|
||||
})
|
||||
.join("\n")
|
||||
: "• —";
|
||||
const mobsLines = result.mobs.length
|
||||
? result.mobs.map(m => `• ${m}`).join('\n')
|
||||
: '• —';
|
||||
? result.mobs.map((m) => `• ${m}`).join("\n")
|
||||
: "• —";
|
||||
const toolInfo = result.tool?.key
|
||||
? `${formatItemLabel(rewardItems.get(result.tool.key) ?? { key: result.tool.key, name: null, icon: null }, { fallbackIcon: '🪓' })}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta ?? 0} dur.)`}`
|
||||
: '—';
|
||||
? `${formatItemLabel(
|
||||
rewardItems.get(result.tool.key) ?? {
|
||||
key: result.tool.key,
|
||||
name: null,
|
||||
icon: null,
|
||||
},
|
||||
{ fallbackIcon: "🪓" }
|
||||
)}${
|
||||
result.tool.broken
|
||||
? " (rota)"
|
||||
: ` (-${result.tool.durabilityDelta ?? 0} dur.)`
|
||||
}`
|
||||
: "—";
|
||||
|
||||
const blocks = [
|
||||
textBlock('# 🌱 Campo'),
|
||||
textBlock("# 🌱 Campo"),
|
||||
dividerBlock(),
|
||||
textBlock(`**Área:** \`${area.key}\`\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`),
|
||||
textBlock(
|
||||
`**Área:** \`${area.key}\`\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`
|
||||
),
|
||||
dividerBlock({ divider: false, spacing: 1 }),
|
||||
textBlock(`**Recompensas**\n${rewardLines}`),
|
||||
dividerBlock({ divider: false, spacing: 1 }),
|
||||
textBlock(`**Eventos**\n${mobsLines}`),
|
||||
];
|
||||
|
||||
// Añadir metadata del área
|
||||
const metaBlocks = buildAreaMetadataBlocks(area);
|
||||
if (metaBlocks.length) {
|
||||
blocks.push(dividerBlock());
|
||||
// @ts-ignore: extended block type allowed at runtime
|
||||
blocks.push(...metaBlocks);
|
||||
}
|
||||
|
||||
const display = buildDisplay(FARM_ACCENT, blocks);
|
||||
await sendDisplayReply(message, display);
|
||||
} catch (e: any) {
|
||||
await message.reply(`❌ No se pudo plantar: ${e?.message ?? e}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Simple Appwrite client wrapper
|
||||
// @ts-ignore
|
||||
import { Client, Databases } from "node-appwrite";
|
||||
import { Client, Databases, Storage } from "node-appwrite";
|
||||
|
||||
const endpoint = process.env.APPWRITE_ENDPOINT || "";
|
||||
const projectId = process.env.APPWRITE_PROJECT_ID || "";
|
||||
@@ -14,8 +14,21 @@ export const APPWRITE_COLLECTION_AI_CONVERSATIONS_ID =
|
||||
export const APPWRITE_COLLECTION_GUILD_CACHE_ID =
|
||||
process.env.APPWRITE_COLLECTION_GUILD_CACHE_ID || "";
|
||||
|
||||
// Optional: collections for game realtime mirrors
|
||||
export const APPWRITE_COLLECTION_QUESTS_ID =
|
||||
process.env.APPWRITE_COLLECTION_QUESTS_ID || "";
|
||||
export const APPWRITE_COLLECTION_QUEST_PROGRESS_ID =
|
||||
process.env.APPWRITE_COLLECTION_QUEST_PROGRESS_ID || "";
|
||||
export const APPWRITE_COLLECTION_SCHEDULED_ATTACKS_ID =
|
||||
process.env.APPWRITE_COLLECTION_SCHEDULED_ATTACKS_ID || "";
|
||||
|
||||
// Optional: bucket for images (areas/levels)
|
||||
export const APPWRITE_BUCKET_IMAGES_ID =
|
||||
process.env.APPWRITE_BUCKET_IMAGES_ID || "";
|
||||
|
||||
let client: Client | null = null;
|
||||
let databases: Databases | null = null;
|
||||
let storage: Storage | null = null;
|
||||
|
||||
function ensureClient() {
|
||||
if (!endpoint || !projectId || !apiKey) return null;
|
||||
@@ -25,6 +38,7 @@ function ensureClient() {
|
||||
.setProject(projectId)
|
||||
.setKey(apiKey);
|
||||
databases = new Databases(client);
|
||||
storage = new Storage(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -32,6 +46,10 @@ export function getDatabases(): Databases | null {
|
||||
return ensureClient() ? (databases as Databases) : null;
|
||||
}
|
||||
|
||||
export function getStorage(): Storage | null {
|
||||
return ensureClient() ? (storage as Storage) : null;
|
||||
}
|
||||
|
||||
export function isAppwriteConfigured(): boolean {
|
||||
return Boolean(
|
||||
endpoint &&
|
||||
@@ -61,3 +79,20 @@ export function isGuildCacheConfigured(): boolean {
|
||||
APPWRITE_COLLECTION_GUILD_CACHE_ID
|
||||
);
|
||||
}
|
||||
|
||||
export function isAppwriteStorageConfigured(): boolean {
|
||||
return Boolean(endpoint && projectId && apiKey && APPWRITE_BUCKET_IMAGES_ID);
|
||||
}
|
||||
|
||||
export function isGameRealtimeConfigured(): boolean {
|
||||
// minimal check for quests/progress and scheduled attacks mirrors
|
||||
return Boolean(
|
||||
endpoint &&
|
||||
projectId &&
|
||||
apiKey &&
|
||||
APPWRITE_DATABASE_ID &&
|
||||
(APPWRITE_COLLECTION_QUESTS_ID ||
|
||||
APPWRITE_COLLECTION_QUEST_PROGRESS_ID ||
|
||||
APPWRITE_COLLECTION_SCHEDULED_ATTACKS_ID)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
export type DisplayBlock =
|
||||
| { kind: 'text'; content: string }
|
||||
| { kind: 'divider'; divider?: boolean; spacing?: number };
|
||||
| { kind: "text"; content: string }
|
||||
| { kind: "divider"; divider?: boolean; spacing?: number }
|
||||
| { kind: "image"; url: string };
|
||||
|
||||
export const textBlock = (content: string): DisplayBlock => ({ kind: 'text', content });
|
||||
export const textBlock = (content: string): DisplayBlock => ({
|
||||
kind: "text",
|
||||
content,
|
||||
});
|
||||
|
||||
export const dividerBlock = (options: { divider?: boolean; spacing?: number } = {}): DisplayBlock => ({
|
||||
kind: 'divider',
|
||||
export const dividerBlock = (
|
||||
options: { divider?: boolean; spacing?: number } = {}
|
||||
): DisplayBlock => ({
|
||||
kind: "divider",
|
||||
divider: options.divider,
|
||||
spacing: options.spacing,
|
||||
});
|
||||
@@ -15,10 +21,15 @@ export function buildDisplay(accentColor: number, blocks: DisplayBlock[]) {
|
||||
type: 17 as const,
|
||||
accent_color: accentColor,
|
||||
components: blocks.map((block) => {
|
||||
if (block.kind === 'text') {
|
||||
if (block.kind === "text") {
|
||||
return { type: 10 as const, content: block.content };
|
||||
}
|
||||
|
||||
if (block.kind === "image") {
|
||||
// This component type will be translated by the renderer to an embed image
|
||||
return { type: 12 as const, url: block.url } as any;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 14 as const,
|
||||
divider: block.divider ?? true,
|
||||
|
||||
@@ -23,6 +23,29 @@ body {
|
||||
animation: glow 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Code blocks enhancements */
|
||||
.code-block pre {
|
||||
background: transparent !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.code-block pre code {
|
||||
display: block;
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Etiquetas visuales API/CLI (apoyadas por header dinámico) */
|
||||
.code-block .is-api {
|
||||
background: linear-gradient(90deg, rgba(99,102,241,0.15), rgba(147,51,234,0.15));
|
||||
}
|
||||
|
||||
.code-block .is-cli {
|
||||
background: linear-gradient(90deg, rgba(236,72,153,0.15), rgba(59,130,246,0.15));
|
||||
}
|
||||
|
||||
/* Scrollbar personalizado (opcional, moderno) */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
|
||||
86
src/server/public/assets/js/code.js
Normal file
86
src/server/public/assets/js/code.js
Normal file
@@ -0,0 +1,86 @@
|
||||
// Mejora de bloques de código: resaltar, barra de acciones (copiar), y etiquetas API/CLI
|
||||
(function () {
|
||||
function enhanceCodeBlocks() {
|
||||
const pres = document.querySelectorAll('pre');
|
||||
pres.forEach((pre) => {
|
||||
if (pre.dataset.enhanced === '1') return;
|
||||
pre.dataset.enhanced = '1';
|
||||
|
||||
// Asegurar que exista <code>
|
||||
let codeEl = pre.querySelector('code');
|
||||
const textRaw = pre.textContent || '';
|
||||
if (!codeEl) {
|
||||
codeEl = document.createElement('code');
|
||||
codeEl.textContent = textRaw;
|
||||
const t = textRaw.trimStart();
|
||||
const isJSON = t.startsWith('{') || t.startsWith('[');
|
||||
const isCLI = /^(!|\$|curl\s|#)/m.test(t);
|
||||
if (isJSON) codeEl.className = 'language-json';
|
||||
else if (isCLI) codeEl.className = 'language-bash';
|
||||
else codeEl.className = 'language-text';
|
||||
pre.textContent = '';
|
||||
pre.appendChild(codeEl);
|
||||
}
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'code-block group relative overflow-hidden rounded-xl border border-white/10 bg-slate-900/70 shadow-lg animate-[slideIn_0.4s_ease-out]';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'flex items-center justify-between px-3 py-2 text-xs bg-slate-800/60 border-b border-white/10';
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.className = 'font-mono tracking-wide text-slate-300';
|
||||
|
||||
const lang = (codeEl.className || '').toLowerCase();
|
||||
let kind = '';
|
||||
if (/bash|shell|sh/.test(lang)) kind = 'CLI';
|
||||
else if (/json|typescript|javascript/.test(lang)) kind = 'API';
|
||||
label.textContent = kind || (lang.replace('language-', '').toUpperCase() || 'CODE');
|
||||
if (kind === 'API') header.classList.add('is-api');
|
||||
else if (kind === 'CLI') header.classList.add('is-cli');
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'flex items-center gap-1';
|
||||
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.type = 'button';
|
||||
copyBtn.className = 'rounded-md px-2 py-1 text-slate-300 hover:text-white hover:bg-white/10 transition';
|
||||
copyBtn.textContent = 'Copiar';
|
||||
copyBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
const text = codeEl.innerText || pre.innerText || '';
|
||||
await navigator.clipboard.writeText(text);
|
||||
copyBtn.textContent = 'Copiado!';
|
||||
setTimeout(() => (copyBtn.textContent = 'Copiar'), 1200);
|
||||
} catch (err) {
|
||||
// silencioso
|
||||
}
|
||||
});
|
||||
|
||||
header.appendChild(label);
|
||||
header.appendChild(actions);
|
||||
actions.appendChild(copyBtn);
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'relative';
|
||||
if (pre.parentNode) pre.parentNode.insertBefore(wrapper, pre);
|
||||
wrapper.appendChild(header);
|
||||
wrapper.appendChild(content);
|
||||
content.appendChild(pre);
|
||||
|
||||
pre.classList.add('overflow-auto');
|
||||
});
|
||||
|
||||
if (window.hljs) {
|
||||
document.querySelectorAll('pre code').forEach((el) => {
|
||||
window.hljs.highlightElement(el);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', enhanceCodeBlocks);
|
||||
} else {
|
||||
enhanceCodeBlocks();
|
||||
}
|
||||
})();
|
||||
@@ -1,210 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Amayo Bot | Guía Completa - Diseño Moderno</title>
|
||||
<meta name="description" content="Guía completa ultra moderna de Amayo Bot para Discord">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
animation: {
|
||||
'gradient': 'gradient 8s linear infinite',
|
||||
'float': 'float 6s ease-in-out infinite',
|
||||
'glow': 'glow 3s ease-in-out infinite',
|
||||
'slide-in': 'slideIn 0.5s ease-out',
|
||||
},
|
||||
keyframes: {
|
||||
gradient: {
|
||||
'0%, 100%': { backgroundPosition: '0% 50%' },
|
||||
'50%': { backgroundPosition: '100% 50%' },
|
||||
},
|
||||
float: {
|
||||
'0%, 100%': { transform: 'translateY(0)' },
|
||||
'50%': { transform: 'translateY(-20px)' },
|
||||
},
|
||||
glow: {
|
||||
'0%, 100%': { opacity: '0.4' },
|
||||
'50%': { opacity: '0.8' },
|
||||
},
|
||||
slideIn: {
|
||||
'0%': { transform: 'translateY(20px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<link rel="stylesheet" href="./assets/css/styles.css">
|
||||
</head>
|
||||
<body class="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-slate-100 antialiased">
|
||||
|
||||
<!-- Animated Background Blobs -->
|
||||
<div class="fixed inset-0 overflow-hidden pointer-events-none z-0">
|
||||
<div class="absolute top-0 left-1/4 w-96 h-96 bg-purple-500/20 rounded-full mix-blend-screen filter blur-3xl animate-float"></div>
|
||||
<div class="absolute top-1/3 right-1/4 w-96 h-96 bg-indigo-500/20 rounded-full mix-blend-screen filter blur-3xl animate-float" style="animation-delay: 2s;"></div>
|
||||
<div class="absolute bottom-0 left-1/3 w-96 h-96 bg-pink-500/15 rounded-full mix-blend-screen filter blur-3xl animate-float" style="animation-delay: 4s;"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10">
|
||||
<!-- Hero Ultra Moderno -->
|
||||
<header class="relative overflow-hidden border-b border-white/5">
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-indigo-500/5 via-purple-500/5 to-transparent"></div>
|
||||
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-20 sm:py-28 relative">
|
||||
<div class="max-w-5xl mx-auto text-center space-y-8">
|
||||
<!-- Badge con Pulse -->
|
||||
<div class="inline-flex items-center gap-3 px-5 py-2.5 rounded-full bg-gradient-to-r from-indigo-500/10 via-purple-500/10 to-pink-500/10 border border-indigo-500/30 backdrop-blur-sm">
|
||||
<span class="relative flex h-2.5 w-2.5">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-indigo-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-gradient-to-r from-indigo-400 to-purple-400"></span>
|
||||
</span>
|
||||
<span class="text-sm font-bold tracking-wider uppercase bg-clip-text text-transparent bg-gradient-to-r from-indigo-200 to-purple-200">
|
||||
Amayo Bot • v0.11.20
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Título con Gradiente Animado -->
|
||||
<h1 class="text-5xl sm:text-6xl md:text-7xl lg:text-8xl font-black leading-tight">
|
||||
<span class="block bg-clip-text text-transparent bg-gradient-to-r from-white via-indigo-100 to-purple-100 animate-gradient bg-[length:200%_auto]">
|
||||
Guía Completa
|
||||
</span>
|
||||
<span class="block bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400 animate-gradient bg-[length:200%_auto]">
|
||||
Amayo Bot
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<!-- Descripción -->
|
||||
<p class="text-xl sm:text-2xl text-slate-300 max-w-3xl mx-auto leading-relaxed font-light">
|
||||
Sistema completo de <span class="font-semibold text-indigo-300">economía</span>, <span class="font-semibold text-purple-300">minijuegos</span>, <span class="font-semibold text-pink-300">misiones</span> y <span class="font-semibold text-blue-300">IA</span> para Discord
|
||||
</p>
|
||||
|
||||
<!-- CTA Buttons Modernos -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center pt-4">
|
||||
<a href="#primeros-pasos" class="group relative px-10 py-4 text-lg font-bold text-white overflow-hidden rounded-2xl transition-all duration-300 hover:scale-105">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-indigo-600 to-purple-600 transition-transform duration-300 group-hover:scale-110"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-indigo-500 to-purple-500 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<span class="relative flex items-center gap-2">
|
||||
Comenzar ahora
|
||||
<svg class="w-5 h-5 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<button id="toggle-nav" class="group px-10 py-4 text-lg font-bold text-white backdrop-blur-xl bg-white/5 border-2 border-white/10 rounded-2xl hover:bg-white/10 hover:border-white/20 hover:scale-105 transition-all duration-300">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
Ver índice
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Meta Info Ultra Moderno -->
|
||||
<div class="flex flex-wrap justify-center gap-3 pt-6">
|
||||
<span class="px-5 py-2 rounded-xl bg-white/5 backdrop-blur-sm border border-white/10 text-sm font-medium text-slate-300 hover:bg-white/10 transition-all">
|
||||
Discord.js 15.0-dev
|
||||
</span>
|
||||
<span class="px-5 py-2 rounded-xl bg-white/5 backdrop-blur-sm border border-white/10 text-sm font-medium text-slate-300 hover:bg-white/10 transition-all">
|
||||
Enero 2025
|
||||
</span>
|
||||
<span class="px-5 py-2 rounded-xl bg-gradient-to-r from-indigo-500/10 to-purple-500/10 border border-indigo-500/30 text-sm font-bold text-indigo-200 hover:from-indigo-500/20 hover:to-purple-500/20 transition-all">
|
||||
23 Secciones Completas
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- TEST: Una sección moderna para ver el estilo -->
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="rounded-3xl bg-gradient-to-br from-slate-800/40 to-slate-900/40 backdrop-blur-2xl border border-white/10 p-8 sm:p-12 shadow-2xl hover:shadow-indigo-500/10 transition-all duration-500 hover:border-indigo-500/30 animate-slide-in">
|
||||
<div class="mb-8">
|
||||
<div class="inline-block">
|
||||
<h2 class="text-4xl sm:text-5xl font-black mb-4 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">
|
||||
🚀 Primeros Pasos
|
||||
</h2>
|
||||
<div class="h-1.5 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-lg sm:text-xl text-slate-300 leading-relaxed mb-10">
|
||||
¡Bienvenido a <span class="font-bold text-white">Amayo Bot</span>! Este bot transforma tu servidor en una experiencia de juego completa.
|
||||
</p>
|
||||
|
||||
<!-- Feature Grid Moderno -->
|
||||
<div class="rounded-2xl bg-gradient-to-br from-indigo-500/10 to-purple-500/10 border border-indigo-500/20 p-8 mb-8">
|
||||
<h3 class="text-2xl font-bold text-indigo-200 mb-6 flex items-center gap-3">
|
||||
<span class="text-3xl">✨</span>
|
||||
Características Principales
|
||||
</h3>
|
||||
<div class="grid gap-4">
|
||||
<div class="flex items-start gap-4 p-4 rounded-xl bg-slate-800/30 border border-slate-700/50 hover:border-indigo-500/50 hover:bg-slate-800/50 transition-all duration-300">
|
||||
<span class="text-2xl">🎮</span>
|
||||
<div>
|
||||
<h4 class="font-bold text-white mb-1">Minijuegos Épicos</h4>
|
||||
<p class="text-sm text-slate-400">Mina, pesca, pelea y cultiva recursos</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4 p-4 rounded-xl bg-slate-800/30 border border-slate-700/50 hover:border-purple-500/50 hover:bg-slate-800/50 transition-all duration-300">
|
||||
<span class="text-2xl">💰</span>
|
||||
<div>
|
||||
<h4 class="font-bold text-white mb-1">Economía Completa</h4>
|
||||
<p class="text-sm text-slate-400">Tienda, crafteo e inventario</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4 p-4 rounded-xl bg-slate-800/30 border border-slate-700/50 hover:border-pink-500/50 hover:bg-slate-800/50 transition-all duration-300">
|
||||
<span class="text-2xl">🏆</span>
|
||||
<div>
|
||||
<h4 class="font-bold text-white mb-1">Progresión & Logros</h4>
|
||||
<p class="text-sm text-slate-400">Misiones, logros y rachas diarias</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Cards -->
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="group p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50 hover:border-indigo-500/50 hover:bg-slate-800/70 transition-all duration-300 hover:scale-105 hover:shadow-xl hover:shadow-indigo-500/10">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<span class="text-4xl">⚡</span>
|
||||
<h3 class="text-xl font-bold text-white">Prefix: <code class="px-3 py-1 rounded-lg bg-indigo-500/20 text-indigo-200 border border-indigo-500/30">!</code></h3>
|
||||
</div>
|
||||
<p class="text-sm text-slate-400">Personalizable con <code class="px-2 py-0.5 rounded bg-slate-700/50 text-slate-300">!configuracion</code></p>
|
||||
</div>
|
||||
|
||||
<div class="group p-6 rounded-2xl bg-slate-800/50 border border-slate-700/50 hover:border-purple-500/50 hover:bg-slate-800/70 transition-all duration-300 hover:scale-105 hover:shadow-xl hover:shadow-purple-500/10">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<span class="text-4xl">❓</span>
|
||||
<h3 class="text-xl font-bold text-white">Ayuda</h3>
|
||||
</div>
|
||||
<p class="text-sm text-slate-400">Usa <code class="px-2 py-0.5 rounded bg-purple-500/20 text-purple-200 border border-purple-500/30">!ayuda</code> para ver comandos</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mensaje para el usuario -->
|
||||
<div class="mt-12 p-8 rounded-3xl bg-gradient-to-r from-indigo-500/10 via-purple-500/10 to-pink-500/10 border border-indigo-500/30 backdrop-blur-sm text-center">
|
||||
<p class="text-2xl font-bold text-white mb-3">✨ Este es el nuevo diseño ultra moderno ✨</p>
|
||||
<p class="text-lg text-slate-300">¿Te gusta este estilo? Todas las 23 secciones tendrán este diseño con:</p>
|
||||
<div class="flex flex-wrap justify-center gap-3 mt-6">
|
||||
<span class="px-4 py-2 rounded-xl bg-white/5 text-sm font-medium text-slate-300">Glassmorphism</span>
|
||||
<span class="px-4 py-2 rounded-xl bg-white/5 text-sm font-medium text-slate-300">Hover Effects</span>
|
||||
<span class="px-4 py-2 rounded-xl bg-white/5 text-sm font-medium text-slate-300">Gradientes</span>
|
||||
<span class="px-4 py-2 rounded-xl bg-white/5 text-sm font-medium text-slate-300">Animaciones</span>
|
||||
<span class="px-4 py-2 rounded-xl bg-white/5 text-sm font-medium text-slate-300">100% Tailwind</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./assets/js/main.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,8 +1,27 @@
|
||||
import { createServer, IncomingMessage, ServerResponse } from "node:http";
|
||||
import { promises as fs } from "node:fs";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { gzipSync } from "node:zlib";
|
||||
import path from "node:path";
|
||||
import ejs from "ejs";
|
||||
|
||||
const publicDir = path.join(__dirname, "public");
|
||||
const viewsDir = path.join(__dirname, "views");
|
||||
// Compresión síncrona (rápida para tamaños pequeños de HTML/CSS/JS)
|
||||
|
||||
// Cargar metadatos del proyecto para usarlos como variables en las vistas
|
||||
let pkg: {
|
||||
name?: string;
|
||||
version?: string;
|
||||
dependencies?: Record<string, string>;
|
||||
} = {};
|
||||
try {
|
||||
const pkgPath = path.join(__dirname, "../../package.json");
|
||||
pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
||||
} catch {
|
||||
// Ignorar si no se puede leer; usaremos valores por defecto
|
||||
}
|
||||
|
||||
const MIME_TYPES: Record<string, string> = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
@@ -21,6 +40,104 @@ const MIME_TYPES: Record<string, string> = {
|
||||
|
||||
const PORT = Number(process.env.PORT) || 3000;
|
||||
|
||||
// --- Basic hardening: blocklist patterns and lightweight rate limiting for suspicious paths ---
|
||||
const BLOCKED_PATTERNS: RegExp[] = [
|
||||
/\b(npci|upi|bhim|aadhaar|aadhar|cts|fastag|bbps|rgcs|nuup|apbs|hdfc|ergo|securities|banking|insurance)\b/i,
|
||||
];
|
||||
|
||||
const SUSP_LENGTH = 18; // long single-segment slugs tend to be bot probes
|
||||
const RATE_WINDOW_MS = 60_000; // 1 minute window
|
||||
const RATE_MAX_SUSPICIOUS = 20; // allow up to 20 suspicious hits per minute per IP
|
||||
|
||||
type Counter = { count: number; resetAt: number };
|
||||
const suspiciousCounters = new Map<string, Counter>();
|
||||
|
||||
function getClientIp(req: IncomingMessage): string {
|
||||
const cf = (req.headers["cf-connecting-ip"] as string) || "";
|
||||
const xff = (req.headers["x-forwarded-for"] as string) || "";
|
||||
const chain = (cf || xff)
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
return (
|
||||
chain[0] || (req.socket && (req.socket as any).remoteAddress) || "unknown"
|
||||
);
|
||||
}
|
||||
|
||||
function isSingleSegment(pathname: string) {
|
||||
// e.g. /foo-bar, no additional slashes
|
||||
return /^\/[A-Za-z0-9._-]+$/.test(pathname);
|
||||
}
|
||||
|
||||
function isSuspiciousPath(pathname: string): boolean {
|
||||
if (
|
||||
!pathname ||
|
||||
pathname === "/" ||
|
||||
pathname === "/index" ||
|
||||
pathname === "/index.html"
|
||||
)
|
||||
return false;
|
||||
if (BLOCKED_PATTERNS.some((re) => re.test(pathname))) return true;
|
||||
if (isSingleSegment(pathname) && pathname.length > SUSP_LENGTH) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function hitSuspicious(ip: string): {
|
||||
allowed: boolean;
|
||||
resetIn: number;
|
||||
remaining: number;
|
||||
} {
|
||||
const now = Date.now();
|
||||
let bucket = suspiciousCounters.get(ip);
|
||||
if (!bucket || now >= bucket.resetAt) {
|
||||
bucket = { count: 0, resetAt: now + RATE_WINDOW_MS };
|
||||
}
|
||||
bucket.count += 1;
|
||||
suspiciousCounters.set(ip, bucket);
|
||||
const allowed = bucket.count <= RATE_MAX_SUSPICIOUS;
|
||||
return {
|
||||
allowed,
|
||||
resetIn: Math.max(0, bucket.resetAt - now),
|
||||
remaining: Math.max(0, RATE_MAX_SUSPICIOUS - bucket.count),
|
||||
};
|
||||
}
|
||||
|
||||
function applySecurityHeaders(base: Record<string, string> = {}) {
|
||||
return {
|
||||
"Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Referrer-Policy": "no-referrer",
|
||||
"X-Frame-Options": "DENY",
|
||||
// Mild CSP to avoid breaking inline styles/scripts already present; adjust as needed
|
||||
"Content-Security-Policy":
|
||||
"default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline' https:; script-src 'self' 'unsafe-inline' https:; font-src 'self' https: data:; frame-src 'self' https://ko-fi.com https://*.ko-fi.com; child-src 'self' https://ko-fi.com https://*.ko-fi.com",
|
||||
...base,
|
||||
};
|
||||
}
|
||||
|
||||
function computeEtag(buf: Buffer): string {
|
||||
// Weak ETag derived from content sha1 and length
|
||||
const hash = createHash("sha1").update(buf).digest("base64");
|
||||
return `W/"${buf.length.toString(16)}-${hash}"`;
|
||||
}
|
||||
|
||||
function acceptsEncoding(req: IncomingMessage, enc: string): boolean {
|
||||
const ae = (req.headers["accept-encoding"] as string) || "";
|
||||
return ae
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.includes(enc);
|
||||
}
|
||||
|
||||
function shouldCompress(mime: string): boolean {
|
||||
return (
|
||||
mime.startsWith("text/") ||
|
||||
mime.includes("json") ||
|
||||
mime.includes("javascript") ||
|
||||
mime.includes("svg")
|
||||
);
|
||||
}
|
||||
|
||||
const resolvePath = (pathname: string): string => {
|
||||
const decoded = decodeURIComponent(pathname);
|
||||
let target = decoded;
|
||||
@@ -37,6 +154,7 @@ const resolvePath = (pathname: string): string => {
|
||||
};
|
||||
|
||||
const sendResponse = async (
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
filePath: string,
|
||||
statusCode = 200
|
||||
@@ -47,59 +165,228 @@ const sendResponse = async (
|
||||
? "no-cache"
|
||||
: "public, max-age=86400, immutable";
|
||||
|
||||
const stat = await fs.stat(filePath).catch(() => undefined);
|
||||
const data = await fs.readFile(filePath);
|
||||
res.writeHead(statusCode, {
|
||||
const etag = computeEtag(data);
|
||||
|
||||
// Conditional requests
|
||||
const inm = (req.headers["if-none-match"] as string) || "";
|
||||
if (inm && inm === etag) {
|
||||
res.writeHead(
|
||||
304,
|
||||
applySecurityHeaders({
|
||||
ETag: etag,
|
||||
"Cache-Control": cacheControl,
|
||||
...(stat ? { "Last-Modified": stat.mtime.toUTCString() } : {}),
|
||||
})
|
||||
);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
let body: any = data;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": mimeType,
|
||||
"Cache-Control": cacheControl,
|
||||
});
|
||||
res.end(data);
|
||||
ETag: etag,
|
||||
...(stat ? { "Last-Modified": stat.mtime.toUTCString() } : {}),
|
||||
};
|
||||
|
||||
if (shouldCompress(mimeType) && acceptsEncoding(req, "gzip")) {
|
||||
try {
|
||||
body = gzipSync(data);
|
||||
headers["Content-Encoding"] = "gzip";
|
||||
headers["Vary"] = "Accept-Encoding";
|
||||
} catch {
|
||||
// Si falla compresión, enviar sin comprimir
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(statusCode, applySecurityHeaders(headers));
|
||||
res.end(body);
|
||||
};
|
||||
|
||||
export const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
||||
try {
|
||||
// 🔒 Forzar HTTPS en producción (Heroku)
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
const proto = req.headers["x-forwarded-proto"];
|
||||
if (proto && proto !== "https") {
|
||||
res.writeHead(301, {
|
||||
Location: `https://${req.headers.host}${req.url}`,
|
||||
});
|
||||
return res.end();
|
||||
}
|
||||
}
|
||||
const renderTemplate = async (
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
template: string,
|
||||
locals: Record<string, any> = {},
|
||||
statusCode = 200
|
||||
) => {
|
||||
const pageFile = path.join(viewsDir, "pages", `${template}.ejs`);
|
||||
const layoutFile = path.join(viewsDir, "layouts", "layout.ejs");
|
||||
const pageBody = await ejs.renderFile(pageFile, locals, { async: true });
|
||||
const defaultTitle = `${
|
||||
locals.appName ?? pkg.name ?? "Amayo Bot"
|
||||
} | Guía Completa`;
|
||||
const html = await ejs.renderFile(
|
||||
layoutFile,
|
||||
{
|
||||
head: null,
|
||||
scripts: null,
|
||||
...locals,
|
||||
title: locals.title ?? defaultTitle,
|
||||
body: pageBody,
|
||||
},
|
||||
{ async: true }
|
||||
);
|
||||
const htmlBuffer = Buffer.from(html, "utf8");
|
||||
const etag = computeEtag(htmlBuffer);
|
||||
|
||||
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
||||
const filePath = resolvePath(url.pathname);
|
||||
|
||||
if (!filePath.startsWith(publicDir)) {
|
||||
res.writeHead(403);
|
||||
res.end("Forbidden");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await sendResponse(res, filePath);
|
||||
} catch (error: any) {
|
||||
if (error.code === "ENOENT") {
|
||||
const notFoundPath = path.join(publicDir, "404.html");
|
||||
try {
|
||||
await sendResponse(res, notFoundPath, 404);
|
||||
} catch {
|
||||
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
||||
res.end("404 - Recurso no encontrado");
|
||||
}
|
||||
} else if (error.code === "EISDIR") {
|
||||
const indexPath = path.join(filePath, "index.html");
|
||||
await sendResponse(res, indexPath);
|
||||
} else {
|
||||
console.error("[Server] Error al servir archivo:", error);
|
||||
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
||||
res.end("500 - Error interno del servidor");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Server] Error inesperado:", error);
|
||||
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
||||
res.end("500 - Error interno");
|
||||
// Conditional ETag for dynamic page (fresh each deploy change)
|
||||
const inm = (req.headers["if-none-match"] as string) || "";
|
||||
if (inm && inm === etag) {
|
||||
res.writeHead(
|
||||
304,
|
||||
applySecurityHeaders({ ETag: etag, "Cache-Control": "no-cache" })
|
||||
);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
let respBody: any = htmlBuffer;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
"Cache-Control": "no-cache",
|
||||
ETag: etag,
|
||||
};
|
||||
|
||||
if (acceptsEncoding(req, "gzip")) {
|
||||
try {
|
||||
respBody = gzipSync(htmlBuffer);
|
||||
headers["Content-Encoding"] = "gzip";
|
||||
headers["Vary"] = "Accept-Encoding";
|
||||
} catch {
|
||||
// continuar sin comprimir
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(statusCode, applySecurityHeaders(headers));
|
||||
res.end(respBody);
|
||||
};
|
||||
|
||||
export const server = createServer(
|
||||
async (req: IncomingMessage, res: ServerResponse) => {
|
||||
try {
|
||||
// 🔒 Forzar HTTPS en producción (Heroku)
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
const proto = req.headers["x-forwarded-proto"];
|
||||
if (proto && proto !== "https") {
|
||||
res.writeHead(301, {
|
||||
Location: `https://${req.headers.host}${req.url}`,
|
||||
});
|
||||
return res.end();
|
||||
}
|
||||
}
|
||||
|
||||
const url = new URL(
|
||||
req.url ?? "/",
|
||||
`http://${req.headers.host ?? "localhost"}`
|
||||
);
|
||||
|
||||
// Basic hardening: respond to robots.txt quickly (optional: disallow all or keep current)
|
||||
if (url.pathname === "/robots.txt") {
|
||||
const robots = "User-agent: *\nAllow: /\n"; // change to Disallow: / if you want to discourage polite crawlers
|
||||
res.writeHead(
|
||||
200,
|
||||
applySecurityHeaders({
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Cache-Control": "public, max-age=86400",
|
||||
})
|
||||
);
|
||||
return res.end(robots);
|
||||
}
|
||||
|
||||
const clientIp = getClientIp(req);
|
||||
if (isSuspiciousPath(url.pathname)) {
|
||||
// Hard block known-bad keyword probes
|
||||
if (BLOCKED_PATTERNS.some((re) => re.test(url.pathname))) {
|
||||
const headers = applySecurityHeaders({
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
});
|
||||
res.writeHead(403, headers);
|
||||
return res.end("Forbidden");
|
||||
}
|
||||
// Rate limit repetitive suspicious hits per IP
|
||||
const rate = hitSuspicious(clientIp);
|
||||
if (!rate.allowed) {
|
||||
const headers = applySecurityHeaders({
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Retry-After": String(Math.ceil(rate.resetIn / 1000)),
|
||||
"X-RateLimit-Limit": String(RATE_MAX_SUSPICIOUS),
|
||||
"X-RateLimit-Remaining": String(rate.remaining),
|
||||
});
|
||||
res.writeHead(429, headers);
|
||||
return res.end("Too Many Requests");
|
||||
}
|
||||
}
|
||||
|
||||
// Ruta dinámica: renderizar index con EJS
|
||||
if (
|
||||
url.pathname === "/" ||
|
||||
url.pathname === "/index" ||
|
||||
url.pathname === "/index.html"
|
||||
) {
|
||||
const now = new Date();
|
||||
const currentDateHuman = now.toLocaleDateString("es-ES", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
const djsVersion = pkg?.dependencies?.["discord.js"] ?? "15.0.0-dev";
|
||||
await renderTemplate(req, res, "index", {
|
||||
appName: pkg.name ?? "Amayo Bot",
|
||||
version: pkg.version ?? "2.0.0",
|
||||
djsVersion,
|
||||
currentDateHuman,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = resolvePath(url.pathname);
|
||||
|
||||
if (!filePath.startsWith(publicDir)) {
|
||||
res.writeHead(403);
|
||||
res.end("Forbidden");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await sendResponse(req, res, filePath);
|
||||
} catch (error: any) {
|
||||
if (error.code === "ENOENT") {
|
||||
const notFoundPath = path.join(publicDir, "404.html");
|
||||
try {
|
||||
await sendResponse(req, res, notFoundPath, 404);
|
||||
} catch {
|
||||
res.writeHead(
|
||||
404,
|
||||
applySecurityHeaders({
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
})
|
||||
);
|
||||
res.end("404 - Recurso no encontrado");
|
||||
}
|
||||
} else if (error.code === "EISDIR") {
|
||||
const indexPath = path.join(filePath, "index.html");
|
||||
await sendResponse(req, res, indexPath);
|
||||
} else {
|
||||
console.error("[Server] Error al servir archivo:", error);
|
||||
res.writeHead(
|
||||
500,
|
||||
applySecurityHeaders({
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
})
|
||||
);
|
||||
res.end("500 - Error interno del servidor");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Server] Error inesperado:", error);
|
||||
res.writeHead(
|
||||
500,
|
||||
applySecurityHeaders({ "Content-Type": "text/plain; charset=utf-8" })
|
||||
);
|
||||
res.end("500 - Error interno");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
108
src/server/views/index.ejs
Normal file
108
src/server/views/index.ejs
Normal file
@@ -0,0 +1,108 @@
|
||||
<% const pageTitle = `${appName} | Guía Completa`; %>
|
||||
<% const head = null; %>
|
||||
<% const scripts = null; %>
|
||||
<% const title = pageTitle; %>
|
||||
<% layout('layouts/layout', { title, head, scripts, appName, version, djsVersion, currentDateHuman }); %>
|
||||
|
||||
<header class="relative overflow-hidden ">
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-indigo-500/5 via-purple-500/5 to-transparent"></div>
|
||||
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-20 sm:py-28 relative">
|
||||
<div class="max-w-5xl mx-auto text-center space-y-8">
|
||||
<div class="inline-flex items-center gap-3 px-5 py-2.5 rounded-full bg-gradient-to-r from-indigo-500/10 via-purple-500/10 to-pink-500/10 border border-indigo-500/30 backdrop-blur-sm">
|
||||
<span class="relative flex h-2.5 w-2.5">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-indigo-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-gradient-to-r from-indigo-400 to-purple-400"></span>
|
||||
</span>
|
||||
<span class="text-sm font-bold tracking-wider uppercase bg-clip-text text-transparent bg-gradient-to-r from-indigo-200 to-purple-200">
|
||||
<%= appName %> • v<%= version %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 class="text-5xl sm:text-6xl md:text-7xl lg:text-8xl font-black leading-tight">
|
||||
<span class="block bg-clip-text text-transparent bg-gradient-to-r from-white via-indigo-100 to-purple-100">
|
||||
Guía Completa
|
||||
</span>
|
||||
<span class="block bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400">
|
||||
<%= appName %>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p class="text-xl sm:text-2xl text-slate-300 max-w-3xl mx-auto leading-relaxed font-light">
|
||||
Sistema completo de <span class="font-semibold text-indigo-300">economía</span>, <span class="font-semibold text-purple-300">minijuegos</span>, <span class="font-semibold text-pink-300">misiones</span> y <span class="font-semibold text-blue-300">IA</span> para Discord
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center pt-4">
|
||||
<a href="#primeros-pasos" class="group relative px-10 py-4 text-lg font-bold text-white overflow-hidden rounded-2xl transition-all duration-300 hover:scale-105">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-indigo-600 to-purple-600 transition-transform duration-300 group-hover:scale-110"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-indigo-500 to-purple-500 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<span class="relative flex items-center gap-2">
|
||||
Comenzar ahora
|
||||
<svg class="w-5 h-5 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<button id="toggle-nav" class="group px-10 py-4 text-lg font-bold text-white backdrop-blur-xl bg-white/5 border-2 border-white/10 rounded-2xl hover:bg-white/10 hover:border-white/20 hover:scale-105 transition-all duration-300">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
Ver índice
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-3 pt-6">
|
||||
<span class="px-5 py-2 rounded-xl bg-white/5 backdrop-blur-sm border border-white/10 text-sm font-medium text-slate-300 hover:bg-white/10 transition-all">
|
||||
Discord.js <%= djsVersion %>
|
||||
</span>
|
||||
<span class="px-5 py-2 rounded-xl bg-white/5 backdrop-blur-sm border border-white/10 text-sm font-medium text-slate-300 hover:bg-white/10 transition-all">
|
||||
<%= currentDateHuman %>
|
||||
</span>
|
||||
<span class="px-5 py-2 rounded-xl bg-gradient-to-r from-indigo-500/10 to-purple-500/10 border border-indigo-500/30 text-sm font-bold text-indigo-200 hover:from-indigo-500/20 hover:to-purple-500/20 transition-all">
|
||||
23 Secciones • Creación de Contenido Incluida
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-12 lg:py-16">
|
||||
<div class="flex min-h-screen flex-col">
|
||||
|
||||
<div class="mx-auto flex w-full max-w-6xl flex-1 flex-col gap-10 px-6 pb-16 lg:flex-row lg:px-10">
|
||||
<%- include('partials/toc') %>
|
||||
|
||||
<main class="flex-1">
|
||||
<div class="mx-auto flex w-full max-w-3xl flex-col gap-8">
|
||||
<%- include('partials/sections/primeros-pasos') %>
|
||||
<%- include('partials/sections/comandos-basicos') %>
|
||||
<%- include('partials/sections/sistema-juego') %>
|
||||
<%- include('partials/sections/minijuegos') %>
|
||||
<%- include('partials/sections/inventario-equipo') %>
|
||||
<%- include('partials/sections/economia') %>
|
||||
<%- include('partials/sections/tienda') %>
|
||||
<%- include('partials/sections/crafteo') %>
|
||||
<%- include('partials/sections/logros') %>
|
||||
<%- include('partials/sections/misiones') %>
|
||||
<%- include('partials/sections/racha') %>
|
||||
<%- include('partials/sections/consumibles') %>
|
||||
<%- include('partials/sections/cofres') %>
|
||||
<%- include('partials/sections/encantamientos') %>
|
||||
<%- include('partials/sections/fundicion') %>
|
||||
<%- include('partials/sections/ia') %>
|
||||
<%- include('partials/sections/recordatorios') %>
|
||||
<%- include('partials/sections/alianzas') %>
|
||||
<%- include('partials/sections/admin') %>
|
||||
<%- include('partials/sections/creacion-contenido') %>
|
||||
<%- include('partials/sections/configuracion') %>
|
||||
<%- include('partials/sections/estadisticas') %>
|
||||
<%- include('partials/sections/tips') %>
|
||||
<%- include('partials/sections/faq') %>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
106
src/server/views/layouts/layout.ejs
Normal file
106
src/server/views/layouts/layout.ejs
Normal file
@@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= title || `${appName} | Guía Completa` %></title>
|
||||
<meta name="description" content="Guía completa de Amayo Bot: comandos, minijuegos, economía, misiones, logros, creación de contenido y más">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- highlight.js (ligero y CDN) -->
|
||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
animation: {
|
||||
'gradient': 'gradient 8s linear infinite',
|
||||
'float': 'float 6s ease-in-out infinite',
|
||||
'glow': 'glow 3s ease-in-out infinite',
|
||||
'slide-in': 'slideIn 0.5s ease-out',
|
||||
},
|
||||
keyframes: {
|
||||
gradient: {
|
||||
'0%, 100%': { backgroundPosition: '0% 50%' },
|
||||
'50%': { backgroundPosition: '100% 50%' },
|
||||
},
|
||||
float: {
|
||||
'0%, 100%': { transform: 'translateY(0)' },
|
||||
'50%': { transform: 'translateY(-20px)' },
|
||||
},
|
||||
glow: {
|
||||
'0%, 100%': { opacity: '0.4' },
|
||||
'50%': { opacity: '0.8' },
|
||||
},
|
||||
slideIn: {
|
||||
'0%': { transform: 'translateY(20px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<link rel="stylesheet" href="/assets/css/styles.css">
|
||||
<% if (typeof head !== 'undefined' && head) { %>
|
||||
<%= head %>
|
||||
<% } %>
|
||||
</head>
|
||||
<body class="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-slate-100 antialiased pt-14">
|
||||
|
||||
<%- await include('../partials/navbar', { appName }) %>
|
||||
|
||||
<!-- Animated Background Blobs -->
|
||||
<div class="fixed inset-0 overflow-hidden pointer-events-none z-0">
|
||||
<div class="absolute top-0 left-1/4 w-96 h-96 bg-purple-500/20 rounded-full mix-blend-screen filter blur-3xl animate-float"></div>
|
||||
<div class="absolute top-1/3 right-1/4 w-96 h-96 bg-indigo-500/20 rounded-full mix-blend-screen filter blur-3xl animate-float" style="animation-delay: 2s;"></div>
|
||||
<div class="absolute bottom-0 left-1/3 w-96 h-96 bg-pink-500/15 rounded-full mix-blend-screen filter blur-3xl animate-float" style="animation-delay: 4s;"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10">
|
||||
<%- body %>
|
||||
</div>
|
||||
|
||||
<footer class="border-t border-white/5 bg-slate-950/80 py-10 text-center">
|
||||
<div class="mx-auto max-w-5xl px-6">
|
||||
<div class="mb-6">
|
||||
<p class="text-2xl font-bold text-white mb-2"><%= appName %></p>
|
||||
<p class="text-sm text-slate-400">Sistema completo de juego, economía y gestión para Discord</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-4 mb-6 text-sm">
|
||||
<a href="#primeros-pasos" class="text-indigo-300 transition hover:text-indigo-200">Primeros Pasos</a>
|
||||
<span class="text-slate-600">•</span>
|
||||
<a href="#comandos-basicos" class="text-indigo-300 transition hover:text-indigo-200">Comandos</a>
|
||||
<span class="text-slate-600">•</span>
|
||||
<a href="#minijuegos" class="text-indigo-300 transition hover:text-indigo-200">Minijuegos</a>
|
||||
<span class="text-slate-600">•</span>
|
||||
<a href="#faq" class="text-indigo-300 transition hover:text-indigo-200">FAQ</a>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-white/5 pt-6">
|
||||
<p class="text-xs text-slate-400 mb-3">
|
||||
Versión <%= version %> • Discord.js <%= djsVersion %> • <%= currentDateHuman %>
|
||||
</p>
|
||||
<p class="text-xs text-slate-500">
|
||||
Amayo © <%= new Date().getFullYear() %> — Documentación para usuarios finales de Discord
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<a class="inline-flex items-center gap-2 text-indigo-300 transition hover:text-indigo-200" href="#primeros-pasos">
|
||||
<span aria-hidden="true">↑</span>
|
||||
Volver arriba
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/assets/js/main.js" type="module"></script>
|
||||
<script src="/assets/js/code.js" defer></script>
|
||||
<% if (typeof scripts !== 'undefined' && scripts) { %>
|
||||
<%= scripts %>
|
||||
<% } %>
|
||||
</body>
|
||||
</html>
|
||||
99
src/server/views/pages/index.ejs
Normal file
99
src/server/views/pages/index.ejs
Normal file
@@ -0,0 +1,99 @@
|
||||
<header class="relative overflow-hidden ">
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-indigo-500/5 via-purple-500/5 to-transparent"></div>
|
||||
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-20 sm:py-28 relative">
|
||||
<div class="max-w-5xl mx-auto text-center space-y-8">
|
||||
<div class="inline-flex items-center gap-3 px-5 py-2.5 rounded-full bg-gradient-to-r from-indigo-500/10 via-purple-500/10 to-pink-500/10 border border-indigo-500/30 backdrop-blur-sm">
|
||||
<span class="relative flex h-2.5 w-2.5">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-indigo-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-gradient-to-r from-indigo-400 to-purple-400"></span>
|
||||
</span>
|
||||
<span class="text-sm font-bold tracking-wider uppercase bg-clip-text text-transparent bg-gradient-to-r from-indigo-200 to-purple-200">
|
||||
<%= appName %> • v<%= version %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 class="text-5xl sm:text-6xl md:text-7xl lg:text-8xl font-black leading-tight">
|
||||
<span class="block bg-clip-text text-transparent bg-gradient-to-r from-white via-indigo-100 to-purple-100">
|
||||
Guía Completa
|
||||
</span>
|
||||
<span class="block bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400">
|
||||
<%= appName %>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p class="text-xl sm:text-2xl text-slate-300 max-w-3xl mx-auto leading-relaxed font-light">
|
||||
Sistema completo de <span class="font-semibold text-indigo-300">economía</span>, <span class="font-semibold text-purple-300">minijuegos</span>, <span class="font-semibold text-pink-300">misiones</span> y <span class="font-semibold text-blue-300">IA</span> para Discord
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center pt-4">
|
||||
<a href="#primeros-pasos" class="group relative px-10 py-4 text-lg font-bold text-white overflow-hidden rounded-2xl transition-all duration-300 hover:scale-105">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-indigo-600 to-purple-600 transition-transform duration-300 group-hover:scale-110"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-indigo-500 to-purple-500 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<span class="relative flex items-center gap-2">
|
||||
Comenzar ahora
|
||||
<svg class="w-5 h-5 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-3 pt-6">
|
||||
<span class="px-5 py-2 rounded-xl bg-white/5 backdrop-blur-sm border border-white/10 text-sm font-medium text-slate-300 hover:bg-white/10 transition-all">
|
||||
Discord.js <%= djsVersion %>
|
||||
</span>
|
||||
<span class="px-5 py-2 rounded-xl bg-white/5 backdrop-blur-sm border border-white/10 text-sm font-medium text-slate-300 hover:bg-white/10 transition-all">
|
||||
<%= currentDateHuman %>
|
||||
</span>
|
||||
<span class="px-5 py-2 rounded-xl bg-gradient-to-r from-indigo-500/10 to-purple-500/10 border border-indigo-500/30 text-sm font-bold text-indigo-200 hover:from-indigo-500/20 hover:to-purple-500/20 transition-all">
|
||||
23 Secciones • Creación de Contenido Incluida
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="mx-auto px-4 sm:px-6 lg:px-8 py-12 lg:py-16 max-w-[120rem]">
|
||||
<div class="flex min-h-screen flex-col">
|
||||
|
||||
<div class="mx-auto flex w-full max-w-[120rem] flex-1 flex-col gap-10 px-0 md:px-6 pb-16 lg:flex-row lg:px-10">
|
||||
<%- await include('../partials/toc') %>
|
||||
|
||||
<main class="w-full flex-1 min-w-0 lg:max-w-none xl:max-w-none 2xl:max-w-none">
|
||||
<div class="mx-auto flex w-full flex-col gap-8">
|
||||
<%- await include('../partials/sections/primeros-pasos') %>
|
||||
<%- await include('../partials/sections/comandos-basicos') %>
|
||||
<%- await include('../partials/sections/ejemplos-basicos') %>
|
||||
<%- await include('../partials/sections/ejemplos-avanzados') %>
|
||||
<%- await include('../partials/sections/sistema-juego') %>
|
||||
<%- await include('../partials/sections/minijuegos') %>
|
||||
<%- await include('../partials/sections/inventario-equipo') %>
|
||||
<%- await include('../partials/sections/economia') %>
|
||||
<%- await include('../partials/sections/tienda') %>
|
||||
<%- await include('../partials/sections/crafteo') %>
|
||||
<%- await include('../partials/sections/logros') %>
|
||||
<%- await include('../partials/sections/misiones') %>
|
||||
<%- await include('../partials/sections/racha') %>
|
||||
<%- await include('../partials/sections/consumibles') %>
|
||||
<%- await include('../partials/sections/cofres') %>
|
||||
<%- await include('../partials/sections/encantamientos') %>
|
||||
<%- await include('../partials/sections/fundicion') %>
|
||||
<%- await include('../partials/sections/ia') %>
|
||||
<%- await include('../partials/sections/recordatorios') %>
|
||||
<%- await include('../partials/sections/alianzas') %>
|
||||
<%- await include('../partials/sections/admin') %>
|
||||
<%- await include('../partials/sections/creacion-contenido') %>
|
||||
<%- await include('../partials/sections/configuracion') %>
|
||||
<%- await include('../partials/sections/estadisticas') %>
|
||||
<%- await include('../partials/sections/tips') %>
|
||||
<%- await include('../partials/sections/faq') %>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<aside class="hidden lg:block lg:w-80 xl:w-96 2xl:w-[28rem] lg:sticky lg:top-24 lg:h-fit lg:self-start">
|
||||
<%- await include('../partials/rightSidebar') %>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
16
src/server/views/partials/navbar.ejs
Normal file
16
src/server/views/partials/navbar.ejs
Normal file
@@ -0,0 +1,16 @@
|
||||
<nav class="fixed inset-x-0 top-0 z-50 border-b border-white/10 bg-slate-950/70 backdrop-blur">
|
||||
<div class="mx-auto max-w-[120rem] px-4 sm:px-6 lg:px-8 h-14 flex items-center justify-between">
|
||||
<a href="#" class="text-white font-bold tracking-wide"><%= appName %></a>
|
||||
<div class="hidden md:flex items-center gap-4 text-sm">
|
||||
<a href="#primeros-pasos" class="text-slate-300 hover:text-white">Guía</a>
|
||||
<a href="#minijuegos" class="text-slate-300 hover:text-white">Minijuegos</a>
|
||||
<a href="#economia" class="text-slate-300 hover:text-white">Economía</a>
|
||||
<a href="#faq" class="text-slate-300 hover:text-white">FAQ</a>
|
||||
</div>
|
||||
<button id="toggle-nav" class="md:hidden text-slate-300 hover:text-white" aria-label="Abrir navegación">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
32
src/server/views/partials/rightSidebar.ejs
Normal file
32
src/server/views/partials/rightSidebar.ejs
Normal file
@@ -0,0 +1,32 @@
|
||||
<div class="space-y-6 xl:space-y-8">
|
||||
<!-- Ko-fi card -->
|
||||
<div class="rounded-2xl border border-white/10 bg-slate-900/70 shadow-2xl shadow-indigo-500/10 overflow-hidden">
|
||||
<div class="p-4 border-b border-white/10">
|
||||
<h3 class="text-sm font-semibold text-slate-200">Apoya el proyecto</h3>
|
||||
</div>
|
||||
<div class="p-2 bg-slate-900">
|
||||
<iframe class="rounded-2xl" id="kofiframe" src="https://ko-fi.com/shnimlz/?hidefeed=true&widget=true&embed=true&preview=true"
|
||||
style="border:none;width:100%;padding:4px;background:#0b1020;"
|
||||
height="712" title="shnimlz"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info cards -->
|
||||
<div class="rounded-2xl border border-indigo-500/20 bg-indigo-500/10 p-4">
|
||||
<h4 class="text-sm font-semibold text-indigo-200 mb-2">Novedades</h4>
|
||||
<ul class="space-y-1 text-xs text-slate-300">
|
||||
<li>• Nueva guía con layout EJS</li>
|
||||
<li>• Mejoras de rendimiento del bot</li>
|
||||
<li>• Sistema de economía ampliado</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-emerald-500/20 bg-emerald-500/10 p-4">
|
||||
<h4 class="text-sm font-semibold text-emerald-200 mb-2">Recursos útiles</h4>
|
||||
<ul class="space-y-1 text-xs text-slate-300">
|
||||
<li><a class="hover:text-white" href="#primeros-pasos">• Primeros pasos</a></li>
|
||||
<li><a class="hover:text-white" href="#economia">• Economía</a></li>
|
||||
<li><a class="hover:text-white" href="#faq">• FAQ</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
4
src/server/views/partials/sections/admin.ejs
Normal file
4
src/server/views/partials/sections/admin.ejs
Normal file
@@ -0,0 +1,4 @@
|
||||
<section id="admin" class="space-y-6 rounded-3xl border border-red-500/20 bg-slate-900/80 p-8 shadow-2xl shadow-red-500/10 backdrop-blur">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">⚙️ Panel de Administración</h2>
|
||||
<p class="text-slate-300 text-sm">Contenido en migración a EJS…</p>
|
||||
</section>
|
||||
4
src/server/views/partials/sections/alianzas.ejs
Normal file
4
src/server/views/partials/sections/alianzas.ejs
Normal file
@@ -0,0 +1,4 @@
|
||||
<section id="alianzas" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🤝 Sistema de Alianzas</h2>
|
||||
<p class="text-slate-300 text-sm">Contenido en migración a EJS…</p>
|
||||
</section>
|
||||
29
src/server/views/partials/sections/cofres.ejs
Normal file
29
src/server/views/partials/sections/cofres.ejs
Normal file
@@ -0,0 +1,29 @@
|
||||
<section id="cofres" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🎁 Cofres y Recompensas</h2>
|
||||
<p class="text-slate-200">Abre cofres para conseguir recompensas aleatorias: monedas, items o incluso roles.</p>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🗝️ Abrir Cofres</h3>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg text-sm">
|
||||
<code class="text-indigo-200">!abrir <itemKey></code>
|
||||
</div>
|
||||
<p class="text-slate-300 text-sm">Ejemplo: <span class="font-mono">!abrir daily_chest</span></p>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">⚙️ Definición de recompensas</h3>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg text-xs overflow-x-auto">
|
||||
<pre class="text-indigo-200 whitespace-pre-wrap">{
|
||||
"chest": {
|
||||
"enabled": true,
|
||||
"rewards": [
|
||||
{ "type": "coins", "amount": 500 },
|
||||
{ "type": "item", "itemKey": "health_potion", "qty": 3 }
|
||||
],
|
||||
"consumeOnOpen": true
|
||||
}
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
26
src/server/views/partials/sections/comandos-basicos.ejs
Normal file
26
src/server/views/partials/sections/comandos-basicos.ejs
Normal file
@@ -0,0 +1,26 @@
|
||||
<section id="comandos-basicos" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10 backdrop-blur">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">⚡ Comandos Básicos</h2>
|
||||
<p class="text-slate-200">
|
||||
Estos son los comandos esenciales que necesitas conocer para empezar.
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white mb-3">📋 Información y Utilidad</h3>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex flex-col gap-1">
|
||||
<code class="rounded bg-indigo-500/15 px-2 py-1 font-mono text-indigo-200 w-fit">!ayuda [comando|categoría]</code>
|
||||
<p class="text-slate-300 pl-2">Muestra la lista de comandos. También puedes usar <code class="text-xs">!help</code>, <code class="text-xs">!comandos</code> o <code class="text-xs">!cmds</code></p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<code class="rounded bg-indigo-500/15 px-2 py-1 font-mono text-indigo-200 w-fit">!ping</code>
|
||||
<p class="text-slate-300 pl-2">Verifica la latencia del bot. También: <code class="text-xs">!latency</code>, <code class="text-xs">!pong</code></p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<code class="rounded bg-indigo-500/15 px-2 py-1 font-mono text-indigo-200 w-fit">!player [@usuario]</code>
|
||||
<p class="text-slate-300 pl-2">Muestra tu perfil completo de jugador con estadísticas, equipo e inventario. También: <code class="text-xs">!perfil</code>, <code class="text-xs">!profile</code>, <code class="text-xs">!yo</code>, <code class="text-xs">!me</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
4
src/server/views/partials/sections/configuracion.ejs
Normal file
4
src/server/views/partials/sections/configuracion.ejs
Normal file
@@ -0,0 +1,4 @@
|
||||
<section id="configuracion" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🔧 Configuración del Servidor</h2>
|
||||
<p class="text-slate-300 text-sm">Contenido en migración a EJS…</p>
|
||||
</section>
|
||||
28
src/server/views/partials/sections/consumibles.ejs
Normal file
28
src/server/views/partials/sections/consumibles.ejs
Normal file
@@ -0,0 +1,28 @@
|
||||
<section id="consumibles" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🍖 Consumibles y Pociones</h2>
|
||||
<p class="text-slate-200">Usa comida y pociones para curarte o ganar ventajas temporales.</p>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🍽️ Uso</h3>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg text-sm">
|
||||
<code class="text-indigo-200">!comer <itemKey></code>
|
||||
</div>
|
||||
<p class="text-slate-300 text-sm">Ejemplo: <span class="font-mono">!comer minor_healing_potion</span></p>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">⚙️ Props JSON útiles</h3>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg text-xs overflow-x-auto">
|
||||
<pre class="text-indigo-200 whitespace-pre-wrap">{
|
||||
"food": { "healHp": 50, "healPercent": 0, "cooldownSeconds": 60 },
|
||||
"stackable": true, "maxInventory": 10
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-2xl border border-amber-500/30 bg-amber-500/10 p-5 text-sm text-amber-100">
|
||||
<strong class="block text-base font-semibold text-amber-200 mb-2">Cooldowns</strong>
|
||||
<p>Algunos consumibles comparten cooldown por categoría. Usa <span class="font-mono">!cooldowns</span> para revisarlos.</p>
|
||||
</div>
|
||||
</section>
|
||||
77
src/server/views/partials/sections/crafteo.ejs
Normal file
77
src/server/views/partials/sections/crafteo.ejs
Normal file
@@ -0,0 +1,77 @@
|
||||
<section id="crafteo" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🛠️ Crafteo</h2>
|
||||
<p class="text-slate-200">Combina materiales para crear objetos. Algunas recetas requieren nivel mínimo o herramientas específicas.</p>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">📜 Ver Recetas</h3>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg text-sm">
|
||||
<code class="text-indigo-200">!recetas</code>
|
||||
</div>
|
||||
<p class="text-slate-300 text-sm">Lista de recetas disponibles y sus requisitos.</p>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🧪 Crear un Objeto</h3>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg text-sm">
|
||||
<code class="text-indigo-200">!craftear <receta> [cantidad]</code>
|
||||
</div>
|
||||
<p class="text-slate-300 text-sm">Ejemplo: <span class="font-mono">!craftear espada 1</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid gap-6 md:grid-cols-2">
|
||||
<div class="rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🧭 Cómo craftear (paso a paso)</h3>
|
||||
<ol class="list-decimal list-inside mt-2 space-y-2 text-slate-300 text-sm">
|
||||
<li>Escribe <span class="font-mono">!recetas</span> y elige el <em>nombre clave</em> de la receta (por ejemplo: <span class="font-mono">espada</span>).</li>
|
||||
<li>Revisa los ingredientes requeridos y tu inventario con <span class="font-mono">!inventario</span>.</li>
|
||||
<li>Si la receta exige nivel/herramienta, verifica tu estado: <span class="font-mono">!perfil</span> y tu equipo con <span class="font-mono">!equipo</span>.</li>
|
||||
<li>Ejecuta <span class="font-mono">!craftear <receta> [cantidad]</span>. Ejemplos:
|
||||
<div class="mt-2 space-y-1">
|
||||
<div class="bg-slate-900/50 p-2 rounded"><code class="text-indigo-200">!craftear espada</code></div>
|
||||
<div class="bg-slate-900/50 p-2 rounded"><code class="text-indigo-200">!craftear espada 3</code></div>
|
||||
</div>
|
||||
</li>
|
||||
<li>Si cumples los requisitos, recibirás el/los objeto(s) al instante en tu inventario.</li>
|
||||
</ol>
|
||||
<p class="text-slate-400 text-xs mt-3">Consejo: si una receta admite múltiples resultados o variantes, en <span class="font-mono">!recetas</span> verás notas adicionales.</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">📌 Requisitos típicos</h3>
|
||||
<ul class="mt-2 space-y-2 text-slate-300 text-sm">
|
||||
<li>Ingredientes exactos en cantidad suficiente.</li>
|
||||
<li>Nivel mínimo del jugador para la receta.</li>
|
||||
<li>Herramienta adecuada equipada (si aplica).</li>
|
||||
</ul>
|
||||
<div class="mt-3 rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3 text-yellow-100 text-xs">
|
||||
Algunas recetas avanzadas podrían requerir materiales raros o pasos previos (p. ej., procesar un material en <em>fundición</em> antes de craftear).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">✅ Mensajes esperados</h3>
|
||||
<ul class="mt-2 space-y-2 text-slate-300 text-sm">
|
||||
<li>Éxito: “Has crafteado <span class="font-mono"><objeto></span> x<span class="font-mono"><cantidad></span>”.</li>
|
||||
<li>Faltan materiales: “No tienes suficientes <span class="font-mono"><material></span>”.</li>
|
||||
<li>Requisito: “Nivel insuficiente para esta receta” o “Necesitas <span class="font-mono"><herramienta></span> equipada”.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-2xl border border-indigo-500/30 bg-indigo-500/10 p-5 text-sm text-indigo-100">
|
||||
<strong class="block text-base font-semibold text-indigo-200 mb-2">Nota</strong>
|
||||
<p>Las recetas pueden actualizarse con nuevos parches. Revisa <span class="font-mono">!recetas</span> después de una actualización.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 rounded-2xl border border-rose-500/30 bg-rose-500/10 p-5 text-sm text-rose-100">
|
||||
<strong class="block text-base font-semibold text-rose-200 mb-2">Errores comunes y cómo resolverlos</strong>
|
||||
<ul class="list-disc list-inside space-y-2">
|
||||
<li><span class="font-semibold">Receta no encontrada:</span> Asegúrate de usar el nombre correcto tal como aparece en <span class="font-mono">!recetas</span>.</li>
|
||||
<li><span class="font-semibold">Faltan materiales:</span> Junta los recursos con minijuegos o compra/intercambia y vuelve a intentar.</li>
|
||||
<li><span class="font-semibold">Nivel insuficiente:</span> Sube de nivel con actividades del juego hasta cumplir el requisito.</li>
|
||||
<li><span class="font-semibold">Herramienta incorrecta o sin durabilidad:</span> Equipa la herramienta adecuada o repárala/sustitúyela.</li>
|
||||
<li><span class="font-semibold">Cantidad demasiado alta:</span> Reduce la cantidad o craftea en varios intentos.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
126
src/server/views/partials/sections/creacion-contenido.ejs
Normal file
126
src/server/views/partials/sections/creacion-contenido.ejs
Normal file
@@ -0,0 +1,126 @@
|
||||
<section id="creacion-contenido" class="rounded-3xl bg-gradient-to-br from-red-900/20 to-orange-900/20 backdrop-blur-xl border border-red-500/30 p-8 shadow-2xl">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-red-200 to-orange-200">🎨 Creación de Contenido</h2>
|
||||
<p class="text-slate-100">Guía técnica paso a paso para crear <strong>items</strong>, <strong>áreas/niveles</strong>, <strong>mobs</strong> y <strong>ofertas</strong> directamente desde Discord. Requiere permiso <span class="font-mono">Manage Guild</span> o rol de staff.</p>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-black/20 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">📦 Items: crear/editar</h3>
|
||||
<div class="bg-black/30 p-3 rounded-lg text-sm">
|
||||
<pre class="text-orange-200 whitespace-pre-wrap">1) Crear base
|
||||
!item-crear iron_sword
|
||||
Base → Nombre: "Espada de Hierro", Descripción, Stackable: false,1
|
||||
Tags → weapon, tier2
|
||||
|
||||
2) Props (JSON) comunes
|
||||
{
|
||||
"tool": { "type": "sword", "tier": 2 },
|
||||
"damage": 15,
|
||||
"breakable": { "enabled": true, "maxDurability": 200 }
|
||||
}
|
||||
|
||||
3) Receta (modal ⭐)
|
||||
Habilitar: true
|
||||
Produce: 1
|
||||
Ingredientes: iron_ingot:3, wood_plank:1
|
||||
|
||||
4) Guardar → ✅ Item creado
|
||||
Prueba: !craftear iron_sword</pre>
|
||||
</div>
|
||||
<p class="text-slate-300 text-sm">Usa <span class="font-mono">!item-editar</span>, <span class="font-mono">!item-ver</span>, <span class="font-mono">!items-lista</span> para gestionar.</p>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-black/20 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🧭 Áreas y Niveles (MINE/LAGOON/FIGHT/FARM)</h3>
|
||||
<div class="bg-black/30 p-3 rounded-lg text-sm">
|
||||
<pre class="text-orange-200 whitespace-pre-wrap">1) Crear área
|
||||
!area-crear mine.iron_cavern
|
||||
Config (JSON): {
|
||||
"cooldownSeconds": 60,
|
||||
"description": "Caverna rica en hierro",
|
||||
"icon": "⛏️"
|
||||
}
|
||||
Guardar → ✅ Área creada
|
||||
|
||||
2) Crear nivel 1
|
||||
!area-nivel mine.iron_cavern 1
|
||||
Requisitos (JSON): {
|
||||
"tool": { "required": true, "toolType": "pickaxe", "minTier": 2 }
|
||||
}
|
||||
Recompensas (JSON): {
|
||||
"draws": 2,
|
||||
"table": [
|
||||
{ "type": "coins", "amount": 50, "weight": 10 },
|
||||
{ "type": "item", "itemKey": "iron_ore", "qty": 2, "weight": 8 }
|
||||
]
|
||||
}
|
||||
Mobs (JSON, opcional): {
|
||||
"draws": 1,
|
||||
"table": [ { "mobKey": "cave_spider", "weight": 10 } ]
|
||||
}
|
||||
Guardar → ✅ Nivel guardado</pre>
|
||||
</div>
|
||||
<div class="bg-black/30 p-3 rounded-lg text-xs">
|
||||
<div class="text-orange-200 font-semibold mb-1">Errores comunes</div>
|
||||
<ul class="list-disc pl-5 space-y-1 text-orange-100">
|
||||
<li><span class="font-mono">mobKey</span> o <span class="font-mono">itemKey</span> inexistente → crea primero o corrige la key</li>
|
||||
<li>Pesos mal balanceados → revisa <span class="font-mono">weight</span> (no negativos; no tienen que sumar 100)</li>
|
||||
<li>Herramienta requerida mal configurada → revisa <span class="font-mono">toolType</span> y <span class="font-mono">minTier</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2 mt-6">
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-black/20 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">👹 Mobs (enemigos/NPCs)</h3>
|
||||
<div class="bg-black/30 p-3 rounded-lg text-sm">
|
||||
<pre class="text-orange-200 whitespace-pre-wrap">1) Crear mob
|
||||
!mob-crear goblin
|
||||
Base → Nombre: Goblin, Categoría: enemies
|
||||
Stats (JSON): { "attack": 10, "hp": 50, "defense": 3, "xpReward": 25 }
|
||||
Drops (JSON): {
|
||||
"draws": 2,
|
||||
"table": [
|
||||
{ "type": "coins", "amount": 20, "weight": 10 },
|
||||
{ "type": "item", "itemKey": "leather", "qty": 1, "weight": 5 }
|
||||
]
|
||||
}
|
||||
Guardar → ✅ Mob creado</pre>
|
||||
</div>
|
||||
<p class="text-slate-300 text-xs">Revisa con <span class="font-mono">!mobs-lista</span> y <span class="font-mono">!mob-eliminar <key></span> si necesitas limpiar datos de prueba.</p>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-black/20 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🛒 Ofertas de Tienda</h3>
|
||||
<div class="bg-black/30 p-3 rounded-lg text-sm">
|
||||
<pre class="text-orange-200 whitespace-pre-wrap">1) Crear oferta
|
||||
!offer-crear
|
||||
Base → itemKey: iron_sword, Habilitada: true
|
||||
Precio (JSON): { "coins": 100 }
|
||||
— o —
|
||||
Precio (JSON): {
|
||||
"coins": 50,
|
||||
"items": [ { "itemKey": "iron_ore", "qty": 5 } ]
|
||||
}
|
||||
Límites → por usuario: 5, stock global: 100
|
||||
Ventana → inicio/fin ISO (opcional)
|
||||
Guardar → ✅ Oferta guardada</pre>
|
||||
</div>
|
||||
<div class="bg-black/30 p-3 rounded-lg text-xs">
|
||||
<div class="text-orange-200 font-semibold mb-1">Errores comunes</div>
|
||||
<ul class="list-disc pl-5 space-y-1 text-orange-100">
|
||||
<li><span class="font-mono">itemKey</span> no existe → crea el ítem primero</li>
|
||||
<li>Formato de precio inválido → respeta estructura de <span class="font-mono">coins</span> e <span class="font-mono">items</span></li>
|
||||
<li>Ventana inválida → usa fechas ISO: <span class="font-mono">YYYY-MM-DDTHH:MM:SSZ</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-2xl border border-orange-500/30 bg-orange-500/10 p-5 text-sm text-orange-100">
|
||||
<strong class="block text-base font-semibold text-orange-200 mb-2">Recomendaciones</strong>
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<li>Usa keys en minúsculas y sin espacios (únicas por servidor).</li>
|
||||
<li>Guarda plantillas de Props JSON para acelerar creación.</li>
|
||||
<li>Prueba inmediatamente tras crear: <span class="font-mono">!craftear</span>, <span class="font-mono">!abrir</span>, <span class="font-mono">!equipar</span>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
31
src/server/views/partials/sections/economia.ejs
Normal file
31
src/server/views/partials/sections/economia.ejs
Normal file
@@ -0,0 +1,31 @@
|
||||
<section id="economia" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10 backdrop-blur">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">💰 Sistema de Economía</h2>
|
||||
<p class="text-slate-200">Gana y gestiona monedas para comprar items, participar en eventos y mejorar tu progreso.</p>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🪙 Ver tus Monedas</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg">
|
||||
<code class="text-indigo-200">!monedas [@usuario]</code>
|
||||
</div>
|
||||
<p class="text-slate-300">Muestra el balance de monedas tuyo o de otro usuario.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">💸 Cómo Ganar Monedas</h3>
|
||||
<ul class="list-disc space-y-1 pl-5 text-sm text-slate-200">
|
||||
<li>Jugar minijuegos (minar, pescar, pelear, plantar)</li>
|
||||
<li>Completar misiones</li>
|
||||
<li>Mantener tu racha diaria</li>
|
||||
<li>Abrir cofres</li>
|
||||
<li>Vender items (si está habilitado)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-amber-500/30 bg-amber-500/10 p-5 text-sm text-amber-100 mt-4">
|
||||
<strong class="block text-base font-semibold text-amber-200 mb-2">⚠️ Importante:</strong>
|
||||
<p>Las monedas son específicas por servidor. Cada servidor de Discord tiene su propia economía independiente.</p>
|
||||
</div>
|
||||
</section>
|
||||
55
src/server/views/partials/sections/ejemplos-avanzados.ejs
Normal file
55
src/server/views/partials/sections/ejemplos-avanzados.ejs
Normal file
@@ -0,0 +1,55 @@
|
||||
<section id="ejemplos-avanzados" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🧪 Ejemplos Avanzados</h2>
|
||||
<p class="text-slate-200">Workflows completos inspirados en tu documentación para staff. Sigue los pasos y copia/pega los JSON cuando se soliciten.</p>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-2 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">1) Sistema de Minería (básico)</h3>
|
||||
<pre class="text-indigo-200 text-sm whitespace-pre-wrap bg-slate-900/40 p-3 rounded-lg"># Ítem Herramienta
|
||||
!item-crear wooden_pickaxe
|
||||
Props (JSON): {"tool": {"type": "pickaxe", "tier": 1}, "breakable": {"enabled": true, "maxDurability": 50, "durabilityPerUse": 1}}
|
||||
|
||||
# Ítem Recurso
|
||||
!item-crear copper_ore
|
||||
Props (JSON): {"craftingOnly": false}
|
||||
|
||||
# Área y Nivel
|
||||
!area-crear mine.starter
|
||||
Config (JSON): {"cooldownSeconds": 30, "icon": "⛏️"}
|
||||
!area-nivel mine.starter 1
|
||||
Requisitos (JSON): {"tool": {"required": true, "toolType": "pickaxe", "minTier": 1}}
|
||||
Recompensas (JSON): {"draws": 2, "table": [{"type":"coins","amount":10,"weight":10},{"type":"item","itemKey":"copper_ore","qty":1,"weight":8}]}</pre>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">2) Cofre de Recompensa Diaria</h3>
|
||||
<pre class="text-indigo-200 text-sm whitespace-pre-wrap bg-slate-900/40 p-3 rounded-lg">!item-crear daily_chest
|
||||
Props (JSON): {
|
||||
"chest": {"enabled": true, "rewards": [
|
||||
{"type": "coins", "amount": 500},
|
||||
{"type": "item", "itemKey": "health_potion", "qty": 3}
|
||||
], "consumeOnOpen": true}
|
||||
}</pre>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">3) Espada Legendaria (cadena resumida)</h3>
|
||||
<pre class="text-indigo-200 text-sm whitespace-pre-wrap bg-slate-900/40 p-3 rounded-lg"># Materiales base → fundición → crafteo
|
||||
!item-crear magic_dust
|
||||
!item-crear steel_ingot
|
||||
# (fundición configurada por el equipo)
|
||||
|
||||
# Producto intermedio
|
||||
!item-crear steel_sword_base
|
||||
Props (JSON): {"tool": {"type": "sword", "tier": 3}, "damage": 25}
|
||||
|
||||
# Encantamiento aplicado
|
||||
!encantar steel_sword_base ruby_core
|
||||
|
||||
# Producto final
|
||||
!item-crear legendary_dragon_slayer
|
||||
Props (JSON): {"damage": 45, "breakable": {"enabled": true, "maxDurability": 300}}
|
||||
Receta (modal): steel_sword_base:1, magic_dust:3, dragon_scale:1</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
47
src/server/views/partials/sections/ejemplos-basicos.ejs
Normal file
47
src/server/views/partials/sections/ejemplos-basicos.ejs
Normal file
@@ -0,0 +1,47 @@
|
||||
<section id="ejemplos-basicos" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🧭 Ejemplos Básicos</h2>
|
||||
<p class="text-slate-200">Un arranque rápido con los comandos más usados. Copia y pega en tu servidor:</p>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">👤 Perfil y progreso</h3>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg text-sm">
|
||||
<pre class="text-indigo-200 whitespace-pre-wrap">!player
|
||||
!stats
|
||||
!logros
|
||||
!misiones
|
||||
!cooldowns</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🎮 Minijuegos</h3>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg text-sm">
|
||||
<pre class="text-indigo-200 whitespace-pre-wrap">!mina
|
||||
!pescar
|
||||
!pelear
|
||||
!plantar</pre>
|
||||
</div>
|
||||
<p class="text-slate-300 text-xs">Tip: Puedes pasar nivel o herramienta, ej. <span class="font-mono">!mina 2 iron_pickaxe</span></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🎒 Inventario y equipo</h3>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg text-sm">
|
||||
<pre class="text-indigo-200 whitespace-pre-wrap">!inventario
|
||||
!equipar weapon iron_sword
|
||||
!comer minor_healing_potion</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">💰 Economía</h3>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg text-sm">
|
||||
<pre class="text-indigo-200 whitespace-pre-wrap">!monedas
|
||||
!tienda
|
||||
!comprar health_potion 2
|
||||
!craftear iron_sword</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
26
src/server/views/partials/sections/encantamientos.ejs
Normal file
26
src/server/views/partials/sections/encantamientos.ejs
Normal file
@@ -0,0 +1,26 @@
|
||||
<section id="encantamientos" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">✨ Encantamientos</h2>
|
||||
<p class="text-slate-200">Aplica mutaciones para mejorar armas, armaduras o herramientas según políticas por ítem.</p>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🪄 Usar encantamientos</h3>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg text-sm">
|
||||
<code class="text-indigo-200">!encantar <itemKey> <mutationKey></code>
|
||||
</div>
|
||||
<p class="text-slate-300 text-sm">Ejemplo: <span class="font-mono">!encantar iron_sword ruby_core</span></p>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">⚙️ Política por item (Props)</h3>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg text-xs overflow-x-auto">
|
||||
<pre class="text-indigo-200 whitespace-pre-wrap">{
|
||||
"mutationPolicy": {
|
||||
"allowedKeys": ["ruby_core", "emerald_core", "sharpness_enchant"],
|
||||
"deniedKeys": ["curse_weakness"]
|
||||
}
|
||||
}</pre>
|
||||
</div>
|
||||
<p class="text-slate-300 text-sm">Define llaves permitidas/prohibidas por ítem.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
4
src/server/views/partials/sections/estadisticas.ejs
Normal file
4
src/server/views/partials/sections/estadisticas.ejs
Normal file
@@ -0,0 +1,4 @@
|
||||
<section id="estadisticas" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">📊 Estadísticas y Progreso</h2>
|
||||
<p class="text-slate-300 text-sm">Contenido en migración a EJS…</p>
|
||||
</section>
|
||||
67
src/server/views/partials/sections/faq.ejs
Normal file
67
src/server/views/partials/sections/faq.ejs
Normal file
@@ -0,0 +1,67 @@
|
||||
<section id="faq" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">❓ Preguntas Frecuentes</h2>
|
||||
<div class="space-y-4 text-slate-200">
|
||||
<details class="group rounded-2xl border border-white/10 bg-slate-900/50 p-4 open:bg-slate-900/60">
|
||||
<summary class="cursor-pointer font-semibold text-white">¿Puedo editar un item después de crearlo?</summary>
|
||||
<div class="mt-2 text-sm">
|
||||
Sí, usa <span class="font-mono">!item-editar <key></span>. Para ver detalles sin editar: <span class="font-mono">!item-ver <key></span>.
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group rounded-2xl border border-white/10 bg-slate-900/50 p-4">
|
||||
<summary class="cursor-pointer font-semibold text-white">¿Cómo elimino un item?</summary>
|
||||
<div class="mt-2 text-sm">
|
||||
Usa <span class="font-mono">!item-eliminar <key></span>. Atención: es permanente y no se puede deshacer.
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group rounded-2xl border border-white/10 bg-slate-900/50 p-4">
|
||||
<summary class="cursor-pointer font-semibold text-white">¿Cómo veo todos los items creados?</summary>
|
||||
<div class="mt-2 text-sm">
|
||||
<span class="font-mono">!items-lista [página]</span> muestra una lista paginada con botones para ver detalles.
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group rounded-2xl border border-white/10 bg-slate-900/50 p-4">
|
||||
<summary class="cursor-pointer font-semibold text-white">¿Qué formato tienen las fechas ISO?</summary>
|
||||
<div class="mt-2 text-sm">
|
||||
Usa <span class="font-mono">YYYY-MM-DDTHH:MM:SSZ</span>. Ejemplos: <span class="font-mono">2025-01-15T00:00:00Z</span>, <span class="font-mono">2025-12-25T23:59:59Z</span>.
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group rounded-2xl border border-white/10 bg-slate-900/50 p-4">
|
||||
<summary class="cursor-pointer font-semibold text-white">¿Puedo crear items globales?</summary>
|
||||
<div class="mt-2 text-sm">
|
||||
Solo los administradores del bot pueden crear items globales. Los items que crees serán locales a tu servidor.
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group rounded-2xl border border-white/10 bg-slate-900/50 p-4">
|
||||
<summary class="cursor-pointer font-semibold text-white">¿Cuántos niveles puedo crear por área?</summary>
|
||||
<div class="mt-2 text-sm">
|
||||
No hay límite técnico; se recomiendan 5–10 por área para una progresión balanceada.
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group rounded-2xl border border-white/10 bg-slate-900/50 p-4">
|
||||
<summary class="cursor-pointer font-semibold text-white">¿Qué pasa si un jugador no tiene la herramienta requerida?</summary>
|
||||
<div class="mt-2 text-sm">
|
||||
El bot indica la herramienta y el tier mínimo necesarios según los requisitos del nivel.
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group rounded-2xl border border-white/10 bg-slate-900/50 p-4">
|
||||
<summary class="cursor-pointer font-semibold text-white">¿Cómo funcionan los pesos (weights)?</summary>
|
||||
<div class="mt-2 text-sm">
|
||||
Son probabilidades relativas. Si A tiene peso 10 y B peso 5, A es el doble de probable (10/15 vs 5/15).
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group rounded-2xl border border-white/10 bg-slate-900/50 p-4">
|
||||
<summary class="cursor-pointer font-semibold text-white">¿Puedo hacer que un ítem cure porcentaje de vida?</summary>
|
||||
<div class="mt-2 text-sm">
|
||||
Sí, en props usa <span class="font-mono">food.healPercent</span> (ej. 50) y un cooldown con <span class="font-mono">food.cooldownSeconds</span>.
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
24
src/server/views/partials/sections/fundicion.ejs
Normal file
24
src/server/views/partials/sections/fundicion.ejs
Normal file
@@ -0,0 +1,24 @@
|
||||
<section id="fundicion" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🔥 Sistema de Fundición</h2>
|
||||
<p class="text-slate-200">Transforma materiales crudos en recursos refinados con tiempo de espera y reclamo.</p>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">⏳ Flujo de uso</h3>
|
||||
<ol class="list-decimal pl-5 space-y-1 text-sm text-slate-200">
|
||||
<li>Inicia: <code class="text-indigo-200">!fundir</code> (ingresa inputs y output)</li>
|
||||
<li>Espera el tiempo indicado</li>
|
||||
<li>Reclama: <code class="text-indigo-200">!fundir-reclamar</code></li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🧪 Receta ejemplo</h3>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg text-xs overflow-x-auto">
|
||||
<pre class="text-indigo-200 whitespace-pre-wrap">Input: copper_ore x5, coal x2
|
||||
Output: copper_ingot x2
|
||||
Duración: 300s</pre>
|
||||
</div>
|
||||
<p class="text-slate-300 text-sm">La configuración exacta la define el equipo en base de datos.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
4
src/server/views/partials/sections/ia.ejs
Normal file
4
src/server/views/partials/sections/ia.ejs
Normal file
@@ -0,0 +1,4 @@
|
||||
<section id="ia" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🤖 Inteligencia Artificial</h2>
|
||||
<p class="text-slate-300 text-sm">Contenido en migración a EJS…</p>
|
||||
</section>
|
||||
48
src/server/views/partials/sections/inventario-equipo.ejs
Normal file
48
src/server/views/partials/sections/inventario-equipo.ejs
Normal file
@@ -0,0 +1,48 @@
|
||||
<section id="inventario-equipo" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10 backdrop-blur">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🎒 Inventario y Equipo</h2>
|
||||
<p class="text-slate-200">
|
||||
Gestiona todos tus items y equipa armas, armaduras y capas para mejorar tus estadísticas.
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white mb-3">📦 Ver tu Inventario</h3>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg">
|
||||
<code class="text-indigo-200">!inventario [página|filtro]</code>
|
||||
<p class="text-xs text-slate-400 mt-1">Aliases: <code class="text-xs">!inv</code></p>
|
||||
</div>
|
||||
<p class="text-slate-300">Muestra todos tus items con cantidades, información de herramientas y estadísticas.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white mb-3">🧤 Equipar Items</h3>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg">
|
||||
<code class="text-indigo-200">!equipar <slot> <itemKey></code>
|
||||
<p class="text-xs text-slate-400 mt-1">Aliases: <code class="text-xs">!equip</code></p>
|
||||
</div>
|
||||
<div class="grid gap-3 md:grid-cols-3 mt-3">
|
||||
<div class="bg-indigo-500/10 border border-indigo-500/30 rounded-lg p-3">
|
||||
<p class="font-semibold text-indigo-200 mb-1">⚔️ weapon</p>
|
||||
<p class="text-xs text-slate-300">Armas que aumentan tu daño (ATK)</p>
|
||||
</div>
|
||||
<div class="bg-indigo-500/10 border border-indigo-500/30 rounded-lg p-3">
|
||||
<p class="font-semibold text-indigo-200 mb-1">🛡️ armor</p>
|
||||
<p class="text-xs text-slate-300">Armaduras que aumentan tu defensa (DEF)</p>
|
||||
</div>
|
||||
<div class="bg-indigo-500/10 border border-indigo-500/30 rounded-lg p-3">
|
||||
<p class="font-semibold text-indigo-200 mb-1">🧥 cape</p>
|
||||
<p class="text-xs text-slate-300">Capas con bonos especiales (HP, stats)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-emerald-500/30 bg-emerald-500/10 p-5 text-sm text-emerald-100">
|
||||
<strong class="block text-base font-semibold text-emerald-200 mb-2">💡 Tip:</strong>
|
||||
<p>Usa <code class="rounded bg-emerald-500/20 px-1.5 py-0.5 font-mono text-xs">!player</code> para ver rápidamente tu equipo actual y las estadísticas que te otorgan.</p>
|
||||
</div>
|
||||
</section>
|
||||
28
src/server/views/partials/sections/logros.ejs
Normal file
28
src/server/views/partials/sections/logros.ejs
Normal file
@@ -0,0 +1,28 @@
|
||||
<section id="logros" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🏆 Logros</h2>
|
||||
<p class="text-slate-200">Completa objetivos para obtener recompensas únicas y mostrar tu progreso.</p>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">📊 Ver tus Logros</h3>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg text-sm">
|
||||
<code class="text-indigo-200">!logros</code>
|
||||
</div>
|
||||
<p class="text-slate-300 text-sm">Muestra logros completados y pendientes, con su progreso.</p>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🎯 Progreso</h3>
|
||||
<ul class="list-disc space-y-1 pl-5 text-sm text-slate-200">
|
||||
<li>Juega minijuegos y combates</li>
|
||||
<li>Completa misiones</li>
|
||||
<li>Alcanza rachas diarias</li>
|
||||
<li>Explora nuevas funciones (crafteo, fundición, etc.)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-2xl border border-fuchsia-500/30 bg-fuchsia-500/10 p-5 text-sm text-fuchsia-100">
|
||||
<strong class="block text-base font-semibold text-fuchsia-200 mb-2">Tip</strong>
|
||||
<p>Algunos logros otorgan títulos o insignias visibles en el servidor. ¡Presúmelos!</p>
|
||||
</div>
|
||||
</section>
|
||||
57
src/server/views/partials/sections/minijuegos.ejs
Normal file
57
src/server/views/partials/sections/minijuegos.ejs
Normal file
@@ -0,0 +1,57 @@
|
||||
<section id="minijuegos" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10 backdrop-blur">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🎯 Minijuegos y Actividades</h2>
|
||||
<p class="text-slate-200">
|
||||
Los minijuegos son la forma principal de ganar recursos, monedas y experiencia. Cada uno tiene su propio estilo y recompensas.
|
||||
</p>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="rounded-2xl border border-orange-500/30 bg-orange-500/5 p-5">
|
||||
<h3 class="text-lg font-semibold text-orange-200 mb-3">⛏️ Minar (Mining)</h3>
|
||||
<div class="space-y-3 text-sm text-slate-200">
|
||||
<p>Ve a la mina y extrae recursos minerales valiosos. Necesitas un pico para minar.</p>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg">
|
||||
<code class="text-indigo-200">!mina [nivel] [herramienta] [area:clave]</code>
|
||||
<p class="text-xs text-slate-400 mt-1">Aliases: <code class="text-xs">!minar</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-cyan-500/30 bg-cyan-500/5 p-5">
|
||||
<h3 class="text-lg font-semibold text-cyan-200 mb-3">🎣 Pescar (Fishing)</h3>
|
||||
<div class="space-y-3 text-sm text-slate-200">
|
||||
<p>Lanza tu caña en la laguna y captura peces y tesoros acuáticos. Necesitas una caña de pescar.</p>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg">
|
||||
<code class="text-indigo-200">!pescar [nivel] [herramienta] [area:clave]</code>
|
||||
<p class="text-xs text-slate-400 mt-1">Aliases: <code class="text-xs">!fish</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-red-500/30 bg-red-500/5 p-5">
|
||||
<h3 class="text-lg font-semibold text-red-200 mb-3">⚔️ Pelear (Combat)</h3>
|
||||
<div class="space-y-3 text-sm text-slate-200">
|
||||
<p>Entra a la arena y enfrenta enemigos peligrosos. Las armas mejoran tu daño.</p>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg">
|
||||
<code class="text-indigo-200">!pelear [nivel] [arma] [area:clave]</code>
|
||||
<p class="text-xs text-slate-400 mt-1">Aliases: <code class="text-xs">!fight</code>, <code class="text-xs">!arena</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-green-500/30 bg-green-500/5 p-5">
|
||||
<h3 class="text-lg font-semibold text-green-200 mb-3">🌾 Plantar/Cultivar (Farming)</h3>
|
||||
<div class="space-y-3 text-sm text-slate-200">
|
||||
<p>Cultiva plantas y cosecha alimentos en tu granja. Usa una azada para mejores resultados.</p>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg">
|
||||
<code class="text-indigo-200">!plantar [nivel] [herramienta]</code>
|
||||
<p class="text-xs text-slate-400 mt-1">Aliases: <code class="text-xs">!farm</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-sky-500/30 bg-sky-500/10 p-5 text-sm text-sky-100 mt-4">
|
||||
<strong class="block text-base font-semibold text-sky-200 mb-2">⏰ Cooldowns:</strong>
|
||||
<p>Cada minijuego tiene un tiempo de espera (cooldown) entre usos. Usa <code class="rounded bg-sky-500/20 px-1.5 py-0.5 font-mono text-xs">!cooldowns</code> para ver tus tiempos activos.</p>
|
||||
</div>
|
||||
</section>
|
||||
34
src/server/views/partials/sections/misiones.ejs
Normal file
34
src/server/views/partials/sections/misiones.ejs
Normal file
@@ -0,0 +1,34 @@
|
||||
<section id="misiones" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">📝 Misiones</h2>
|
||||
<p class="text-slate-200">Tareas con objetivos y recompensas. Úsalas para guiar la progresión diaria y semanal.</p>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">📋 Ver y Reclamar</h3>
|
||||
<ul class="list-disc space-y-1 pl-5 text-sm text-slate-200">
|
||||
<li><code class="text-indigo-200">!misiones</code> — Ver misiones disponibles</li>
|
||||
<li><code class="text-indigo-200">!mision-reclamar <key></code> — Reclamar recompensa</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🛠️ Crear como Admin</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg overflow-x-auto">
|
||||
<pre class="text-indigo-200 whitespace-pre-wrap">!mision-crear daily_mining_quest
|
||||
Base: Nombre, Descripción, Tipo (daily/weekly/one_time)
|
||||
Requisitos (JSON): { "type": "mine_count", "count": 10 }
|
||||
Recompensas (JSON): { "coins": 1000, "xp": 500 }</pre>
|
||||
</div>
|
||||
<p class="text-slate-300">Edita desde Discord con botones y modales; no necesitas código.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-2xl border border-sky-500/30 bg-sky-500/10 p-5 text-sm text-sky-100">
|
||||
<strong class="block text-base font-semibold text-sky-200 mb-2">Tipos y requisitos comunes</strong>
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<li><span class="text-white">Tipos:</span> daily, weekly, one_time, repeatable</li>
|
||||
<li><span class="text-white">Requisitos:</span> mine_count, fish_count, fight_count, collect_items, defeat_mobs</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
39
src/server/views/partials/sections/primeros-pasos.ejs
Normal file
39
src/server/views/partials/sections/primeros-pasos.ejs
Normal file
@@ -0,0 +1,39 @@
|
||||
<section id="primeros-pasos" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10 backdrop-blur">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🚀 Primeros Pasos</h2>
|
||||
<p class="text-slate-200">
|
||||
¡Bienvenido a <strong class="text-white">Amayo Bot</strong>! Este bot transforma tu servidor de Discord en una experiencia de juego completa con economía, minijuegos, misiones y mucho más.
|
||||
</p>
|
||||
|
||||
<div class="space-y-4 rounded-2xl border border-indigo-500/30 bg-indigo-500/10 p-5 text-slate-200">
|
||||
<h3 class="text-lg font-semibold text-indigo-200">✨ ¿Qué puedes hacer con Amayo?</h3>
|
||||
<ul class="list-disc space-y-2 pl-5 text-sm">
|
||||
<li><strong class="text-white">Jugar Minijuegos:</strong> Mina recursos, pesca, pelea contra enemigos y cultiva en granjas</li>
|
||||
<li><strong class="text-white">Economía Completa:</strong> Gana monedas, compra en la tienda, craftea items y gestiona tu inventario</li>
|
||||
<li><strong class="text-white">Sistema de Progresión:</strong> Sube de nivel, completa misiones, desbloquea logros y mantén tu racha diaria</li>
|
||||
<li><strong class="text-white">Personalización:</strong> Equipa armas, armaduras y capas para mejorar tus estadísticas</li>
|
||||
<li><strong class="text-white">IA Conversacional:</strong> Chatea con Gemini AI directamente desde Discord</li>
|
||||
<li><strong class="text-white">Sistema de Alianzas:</strong> Comparte enlaces de invitación y gana puntos para tu servidor</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2 py-5">
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">⚡ Prefix del Bot</h3>
|
||||
<p class="text-sm text-slate-200 py-5">
|
||||
El prefix por defecto es <code class="rounded bg-indigo-500/15 px-2 py-1 font-mono text-sm text-indigo-200">!</code>
|
||||
</p>
|
||||
<p class="text-xs text-slate-300 mt-2">
|
||||
Los administradores pueden cambiarlo con <code class="rounded bg-indigo-500/15 px-1.5 py-0.5 font-mono text-xs text-indigo-200">!configuracion</code>
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">❓ Obtener Ayuda</h3>
|
||||
<p class="text-sm text-slate-200">
|
||||
Usa <code class="rounded bg-indigo-500/15 px-2 py-1 font-mono text-sm text-indigo-200">!ayuda</code> para ver todos los comandos disponibles
|
||||
</p>
|
||||
<p class="text-xs text-slate-300 mt-2">
|
||||
También puedes usar <code class="rounded bg-indigo-500/15 px-1.5 py-0.5 font-mono text-xs text-indigo-200">!ayuda <comando></code> para detalles específicos
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
22
src/server/views/partials/sections/racha.ejs
Normal file
22
src/server/views/partials/sections/racha.ejs
Normal file
@@ -0,0 +1,22 @@
|
||||
<section id="racha-diaria" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🔥 Racha Diaria</h2>
|
||||
<p class="text-slate-200">Entra cada día y realiza acciones para mantener tu racha. A mayor racha, mejores recompensas.</p>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">📆 Comandos útiles</h3>
|
||||
<ul class="list-disc pl-5 space-y-1 text-sm text-slate-200">
|
||||
<li><code class="text-indigo-200">!racha</code> — Ver tu racha actual</li>
|
||||
<li><code class="text-indigo-200">!cooldowns</code> — Revisa tiempos de espera de minijuegos</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🎁 Beneficios</h3>
|
||||
<ul class="list-disc pl-5 space-y-1 text-sm text-slate-200">
|
||||
<li>Bonos de monedas diarios o semanales</li>
|
||||
<li>Acceso a cofres o misiones especiales</li>
|
||||
<li>Progreso extra en logros</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
4
src/server/views/partials/sections/recordatorios.ejs
Normal file
4
src/server/views/partials/sections/recordatorios.ejs
Normal file
@@ -0,0 +1,4 @@
|
||||
<section id="recordatorios" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">⏰ Sistema de Recordatorios</h2>
|
||||
<p class="text-slate-300 text-sm">Contenido en migración a EJS…</p>
|
||||
</section>
|
||||
36
src/server/views/partials/sections/sistema-juego.ejs
Normal file
36
src/server/views/partials/sections/sistema-juego.ejs
Normal file
@@ -0,0 +1,36 @@
|
||||
<section id="sistema-juego" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10 backdrop-blur">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🎮 Sistema de Juego</h2>
|
||||
<p class="text-slate-200">
|
||||
El sistema de juego de Amayo incluye <strong class="text-white">HP (puntos de vida)</strong>, <strong class="text-white">estadísticas de combate</strong>, <strong class="text-white">niveles de progresión</strong> y más.
|
||||
</p>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">⚔️ Estadísticas de Combate</h3>
|
||||
<ul class="list-disc space-y-1 pl-5 text-sm text-slate-200">
|
||||
<li><strong class="text-white">HP (Vida):</strong> Tus puntos de vida actuales y máximos</li>
|
||||
<li><strong class="text-white">ATK (Ataque):</strong> Daño que infliges a los enemigos</li>
|
||||
<li><strong class="text-white">DEF (Defensa):</strong> Reduce el daño recibido</li>
|
||||
<li><strong class="text-white">Bonos de Equipo:</strong> Las armas, armaduras y capas mejoran tus stats</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">📊 Ver tus Estadísticas</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex flex-col gap-1">
|
||||
<code class="rounded bg-indigo-500/15 px-2 py-1 font-mono text-indigo-200 w-fit">!player</code>
|
||||
<p class="text-slate-300">Vista general de tu perfil</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<code class="rounded bg-indigo-500/15 px-2 py-1 font-mono text-indigo-200 w-fit">!stats</code>
|
||||
<p class="text-slate-300">Estadísticas detalladas de todas tus actividades</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-amber-500/30 bg-amber-500/10 p-5 text-sm text-amber-100">
|
||||
<strong class="block text-base font-semibold text-amber-200 mb-2">💡 Consejo:</strong>
|
||||
<p>Equipa mejores armas y armaduras para aumentar tus estadísticas y tener más éxito en los minijuegos de combate.</p>
|
||||
</div>
|
||||
</section>
|
||||
28
src/server/views/partials/sections/tienda.ejs
Normal file
28
src/server/views/partials/sections/tienda.ejs
Normal file
@@ -0,0 +1,28 @@
|
||||
<section id="tienda" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl shadow-indigo-500/10">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">🛒 Tienda</h2>
|
||||
<p class="text-slate-200">Compra y vende objetos utilizando tus monedas. La disponibilidad puede variar según el servidor.</p>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🧾 Ver Catálogo</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg">
|
||||
<code class="text-indigo-200">!tienda</code>
|
||||
</div>
|
||||
<p class="text-slate-300">Muestra la lista de items disponibles actualmente.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3 rounded-2xl border border-white/5 bg-slate-900/60 p-5">
|
||||
<h3 class="text-lg font-semibold text-white">🛍️ Comprar Items</h3>
|
||||
<div class="bg-slate-900/50 p-3 rounded-lg text-sm">
|
||||
<code class="text-indigo-200">!comprar <item> [cantidad]</code>
|
||||
</div>
|
||||
<p class="text-slate-300 text-sm">Ejemplo: <span class="font-mono">!comprar pocion 3</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 rounded-2xl border border-emerald-500/30 bg-emerald-500/10 p-5 text-sm text-emerald-100">
|
||||
<strong class="block text-base font-semibold text-emerald-200 mb-2">Consejo</strong>
|
||||
<p>Algunos items tienen descuentos temporales o packs especiales. ¡Revisa la tienda con frecuencia!</p>
|
||||
</div>
|
||||
</section>
|
||||
4
src/server/views/partials/sections/tips.ejs
Normal file
4
src/server/views/partials/sections/tips.ejs
Normal file
@@ -0,0 +1,4 @@
|
||||
<section id="tips" class="rounded-3xl bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-xl border border-white/10 p-8 shadow-2xl">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold mb-3 bg-clip-text text-transparent bg-gradient-to-r from-white to-indigo-200">💡 Tips y Trucos</h2>
|
||||
<p class="text-slate-300 text-sm">Contenido en migración a EJS…</p>
|
||||
</section>
|
||||
32
src/server/views/partials/toc.ejs
Normal file
32
src/server/views/partials/toc.ejs
Normal file
@@ -0,0 +1,32 @@
|
||||
<nav id="toc" class="hidden lg:block w-full max-w-sm rounded-3xl border border-white/10 bg-slate-900/80 text-left shadow-2xl shadow-indigo-500/20 lg:sticky lg:top-24 lg:max-h-[calc(100vh-6rem)] lg:w-80 lg:overflow-y-auto">
|
||||
<div class="text-xs p-6 font-semibold uppercase tracking-[0.3em] text-slate-400">
|
||||
Índice de Contenidos
|
||||
</div>
|
||||
<ul class="ps-8 mt-4 space-y-4 text-sm">
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#primeros-pasos">🚀 Primeros Pasos</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#comandos-basicos">⚡ Comandos Básicos</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#ejemplos-basicos">🧭 Ejemplos Básicos</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#sistema-juego">🎮 Sistema de Juego</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#minijuegos">🎯 Minijuegos y Actividades</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#inventario-equipo">🎒 Inventario y Equipo</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#economia">💰 Sistema de Economía</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#tienda">🛒 Tienda y Compras</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#crafteo">🛠️ Crafteo y Creación</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#logros">🏆 Logros</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#misiones">📝 Misiones</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#racha-diaria">🔥 Racha Diaria</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#consumibles">🍖 Consumibles y Pociones</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#cofres">🎁 Cofres y Recompensas</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#encantamientos">✨ Encantamientos</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#fundicion">🔥 Fundición</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#ia">🤖 Inteligencia Artificial</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#recordatorios">⏰ Recordatorios</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#alianzas">🤝 Sistema de Alianzas</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#admin">⚙️ Administración (Admin)</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#configuracion">🔧 Configuración Servidor</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#ejemplos-avanzados">🧪 Ejemplos Avanzados</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#estadisticas">📊 Estadísticas</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#tips">💡 Tips y Trucos</a></li>
|
||||
<li><a class="text-slate-200 transition hover:text-indigo-300" href="#faq">❓ Preguntas Frecuentes</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
Reference in New Issue
Block a user