From 40625a9f7468cc7ba84ad3814955ba1ca77404fe Mon Sep 17 00:00:00 2001 From: shni Date: Sun, 5 Oct 2025 22:58:56 -0500 Subject: [PATCH] feat: implement basic HTTP server with static file serving and error handling --- src/main.ts | 5 +- src/server/README.md | 78 ++ src/server/public/404.html | 46 + src/server/public/assets/css/styles.css | 407 ++++++++ src/server/public/assets/js/main.js | 70 ++ src/server/public/index.html | 1234 +++++++++++++++++++++++ src/server/server.ts | 105 ++ 7 files changed, 1944 insertions(+), 1 deletion(-) create mode 100644 src/server/README.md create mode 100644 src/server/public/404.html create mode 100644 src/server/public/assets/css/styles.css create mode 100644 src/server/public/assets/js/main.js create mode 100644 src/server/public/index.html create mode 100644 src/server/server.ts diff --git a/src/main.ts b/src/main.ts index 8284532..e118b59 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,6 +11,7 @@ import { startReminderPoller } from "./core/api/reminders"; import { ensureRemindersSchema } from "./core/api/remindersSchema"; import logger from "./core/lib/logger"; import { applyModalSubmitInteractionPatch } from "./core/patches/discordModalPatch"; +import { server } from "./server/server"; // Activar monitor de memoria si se define la variable const __memInt = parseInt(process.env.MEMORY_LOG_INTERVAL_SECONDS || '0', 10); @@ -163,7 +164,9 @@ process.on('SIGTERM', gracefulShutdown); async function bootstrap() { logger.info("🚀 Iniciando bot..."); - + await server.listen(process.env.PORT || 3000, () => { + logger.info(`📘 Amayo Docs disponible en http://localhost:${process.env.PORT || 3000}`); + }); // Cargar recursos locales (no deberían tirar el proceso si fallan) try { loadCommands(); } catch (e) { logger.error({ err: e }, 'Error cargando comandos'); } try { loadComponents(); } catch (e) { logger.error({ err: e }, 'Error cargando componentes'); } diff --git a/src/server/README.md b/src/server/README.md new file mode 100644 index 0000000..c5a918f --- /dev/null +++ b/src/server/README.md @@ -0,0 +1,78 @@ +# Amayo Docs (Static) + +Sitio web estático para documentar el flujo de creación de contenido dentro del bot Amayo. Incluye guías para items, mobs, áreas, niveles, logros, misiones, cofres, crafteos, mutaciones y consumibles. + +## 🚀 Características + +- UI moderna en una sola página con navegación responsiva. +- Plantillas JSON listas para copiar en los modales del bot. +- Resumen de servicios principales (`EconomyService`, `MinigamesService`). +- Servidor HTTP minimalista (sin dependencias externas) pensado para Heroku. + +## 📦 Estructura + +``` +server/ +├── Procfile # Entrada para Heroku (web: npm start) +├── package.json # Scripts y metadata del mini proyecto +├── server.js # Servidor Node para archivos estáticos +├── public/ +│ ├── index.html # Página principal con toda la documentación +│ ├── 404.html # Página de error +│ └── assets/ +│ ├── css/styles.css +│ └── js/main.js +└── README.md # Este archivo +``` + +## 🛠️ Uso local + +```bash +cd server +npm install # (opcional, no se instalan paquetes pero genera package-lock) +npm start +``` + +El sitio quedará disponible en `http://localhost:3000`. + +## ☁️ Despliegue en Heroku + +### 1. Crear una app nueva + +```bash +heroku create amayo-docs +``` + +### 2. Empujar solo la carpeta `server` + +```bash +git subtree push --prefix server heroku main +``` + +> Si prefieres desplegar desde otra rama, reemplaza `main` por la rama deseada. + +### 3. Variables recomendadas + +```bash +heroku config:set NODE_ENV=production -a amayo-docs +``` + +La app usará el `Procfile` incluido (`web: npm start`). + +## 🔍 Validación + +Para asegurarte de que el servidor arranca sin errores de sintaxis: + +```bash +node --check server/server.js +``` + +## 🧭 Próximos pasos sugeridos + +- Añadir ejemplos visuales (capturas o diagramas) en `public/assets/img/`. +- Integrar métricas básicas (por ejemplo, contador simple con Cloudflare Analytics). +- Automatizar despliegue usando GitHub Actions + Heroku API. + +--- + +Made with ❤ para la comunidad de administradores que usan Amayo. diff --git a/src/server/public/404.html b/src/server/public/404.html new file mode 100644 index 0000000..98a5da6 --- /dev/null +++ b/src/server/public/404.html @@ -0,0 +1,46 @@ + + + + + + Página no encontrada | Amayo Docs + + + + +
+

404

+

No encontramos la página que buscabas.

+ Regresar al índice +
+ + diff --git a/src/server/public/assets/css/styles.css b/src/server/public/assets/css/styles.css new file mode 100644 index 0000000..5442f18 --- /dev/null +++ b/src/server/public/assets/css/styles.css @@ -0,0 +1,407 @@ +:root { + color-scheme: dark; +} + +body { + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; +} + +code { + font-feature-settings: "calt" 0; +}:root { + color-scheme: light dark; + --bg: #0f172a; + --bg-soft: rgba(15, 23, 42, 0.6); + --bg-card: rgba(15, 23, 42, 0.85); + --bg-card-light: #f8fafc; + --text: #0f172a; + --text-light: #f8fafc; + --accent: #6366f1; + --accent-soft: rgba(99, 102, 241, 0.15); + --border: rgba(148, 163, 184, 0.2); + --success: #22c55e; + --warning: #f59e0b; + --info: #0ea5e9; + --card-shadow: 0 24px 48px -24px rgba(15, 23, 42, 0.45); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; + background: linear-gradient(180deg, #020617 0%, #0f172a 55%, #111827 100%); + color: var(--text-light); + min-height: 100vh; + line-height: 1.6; + display: flex; + flex-direction: column; +} + +a { + color: inherit; +} + +a:hover { + color: var(--accent); +} + +.hero { + padding: clamp(3rem, 8vw, 6rem) clamp(2rem, 6vw, 5rem) clamp(2rem, 6vw, 4rem); + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.hero::before { + content: ""; + position: absolute; + inset: 0; + background: radial-gradient(circle at 10% 20%, rgba(99, 102, 241, 0.35), transparent 55%), + radial-gradient(circle at 80% 10%, rgba(56, 189, 248, 0.25), transparent 60%), + radial-gradient(circle at 20% 80%, rgba(236, 72, 153, 0.15), transparent 65%); + opacity: 0.75; + z-index: 0; +} + +.hero__content { + max-width: 60rem; + position: relative; + z-index: 1; +} + +.hero h1 { + font-size: clamp(2.5rem, 4vw, 3.8rem); + margin-bottom: 1.2rem; + font-weight: 700; +} + +.hero p { + max-width: 45rem; + font-size: 1.1rem; + color: rgba(248, 250, 252, 0.85); + margin-left: auto; + margin-right: auto; +} + +.hero__cta { + margin-top: 2.5rem; + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: center; +} + +.hero__meta { + margin-top: 2rem; + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + position: relative; + z-index: 1; + justify-content: center; +} + +.layout { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: clamp(1.5rem, 5vw, 3rem); + padding: 0 clamp(1.5rem, 6vw, 4rem) 4rem; +} + +.badge { + background: rgba(15, 23, 42, 0.55); + border: 1px solid rgba(148, 163, 184, 0.35); + border-radius: 999px; + padding: 0.45rem 0.9rem; + font-size: 0.85rem; + letter-spacing: 0.02em; +} + +.btn { + border: none; + border-radius: 999px; + padding: 0.75rem 1.6rem; + font-weight: 600; + text-decoration: none; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.btn.primary { + background: linear-gradient(90deg, var(--accent) 0%, #7c3aed 100%); + color: white; + box-shadow: 0 18px 36px -18px rgba(99, 102, 241, 0.85); +} + +.btn.ghost { + background: rgba(15, 23, 42, 0.45); + border: 1px solid rgba(148, 163, 184, 0.4); + color: rgba(248, 250, 252, 0.9); +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 22px 40px -24px rgba(99, 102, 241, 0.9); +} + +.toc { + position: sticky; + top: 1.5rem; + margin: 2rem auto; + padding: 1.5rem; + background: rgba(15, 23, 42, 0.5); + border: 1px solid var(--border); + border-radius: 1.25rem; + backdrop-filter: blur(24px); + width: min(90vw, 22rem); + box-shadow: var(--card-shadow); +} + +.toc__title { + font-weight: 600; + margin-bottom: 1rem; + letter-spacing: 0.05em; + text-transform: uppercase; + font-size: 0.9rem; + color: rgba(148, 163, 184, 0.85); +} + +.toc ul { + list-style: none; + display: grid; + gap: 0.65rem; +} + +.toc a { + text-decoration: none; + color: rgba(226, 232, 240, 0.95); + font-size: 0.95rem; + transition: color 0.2s ease; +} + +.content { + margin: 2rem auto 5rem; + display: flex; + flex-direction: column; + gap: 2.5rem; + width: min(1100px, calc(100% - 4rem)); + align-items: center; +} + +.card { + padding: clamp(1.8rem, 3vw, 2.4rem); + border-radius: 1.5rem; + background: var(--bg-card); + border: 1px solid rgba(148, 163, 184, 0.2); + box-shadow: var(--card-shadow); + width: min(100%, 960px); + text-align: center; +} + +.card h2 { + font-size: clamp(1.8rem, 2.8vw, 2.2rem); + margin-bottom: 1rem; +} + +.card h3 { + font-size: 1.15rem; + margin-bottom: 0.75rem; +} + +.card p { + color: rgba(226, 232, 240, 0.9); + margin-bottom: 1rem; + margin-left: auto; + margin-right: auto; + max-width: 48rem; +} + +.card ul { + padding-left: 1.25rem; + margin-bottom: 1rem; + text-align: left; + margin-left: auto; + margin-right: auto; + max-width: 48rem; +} + +.card li + li { + margin-top: 0.5rem; +} + +.card__sub { + padding: 1.4rem; + border-radius: 1rem; + background: rgba(15, 23, 42, 0.55); + border: 1px solid rgba(148, 163, 184, 0.14); + text-align: left; + width: 100%; +} + +.grid { + display: grid; + gap: 1.25rem; + justify-items: center; +} + +.grid.two { + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); +} + +pre { + background: rgba(15, 23, 42, 0.8); + color: rgba(244, 244, 255, 0.95); + padding: 1rem 1.25rem; + border-radius: 1rem; + overflow-x: auto; + border: 1px solid rgba(99, 102, 241, 0.25); + font-size: 0.9rem; + margin: 0 auto 1rem; + max-width: 48rem; +} + +code { + font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular, SFMono, + "Segoe UI Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + padding: 0.15rem 0.35rem; + border-radius: 0.5rem; + background: rgba(99, 102, 241, 0.18); + color: #cbd5f5; +} + +.card details { + background: rgba(15, 23, 42, 0.55); + border: 1px solid rgba(148, 163, 184, 0.14); + padding: 1rem 1.25rem; + border-radius: 1rem; + color: rgba(226, 232, 240, 0.9); + text-align: left; +} + +.card details + details { + margin-top: 1rem; +} + +.card details summary { + font-weight: 600; + cursor: pointer; + list-style: none; +} + +.card details[open] summary { + color: rgba(129, 140, 248, 0.95); +} + +.callout { + border-radius: 1.2rem; + padding: 1.25rem 1.5rem; + border: 1px solid transparent; + margin-top: 1.25rem; + display: flex; + gap: 1rem; +} + +.callout strong { + font-weight: 700; +} + +.callout.info { + background: rgba(14, 165, 233, 0.12); + border-color: rgba(14, 165, 233, 0.32); + color: rgba(125, 211, 252, 0.95); +} + +.callout.success { + background: rgba(34, 197, 94, 0.12); + border-color: rgba(34, 197, 94, 0.32); + color: rgba(134, 239, 172, 0.95); +} + +.callout.warning { + background: rgba(245, 158, 11, 0.12); + border-color: rgba(245, 158, 11, 0.35); + color: rgba(253, 224, 71, 0.95); +} + +.footer { + margin-top: auto; + padding: 3rem 1.5rem; + text-align: center; + color: rgba(203, 213, 225, 0.7); + font-size: 0.95rem; +} + +.footer a { + display: inline-block; + margin-top: 0.75rem; + color: rgba(129, 140, 248, 0.9); +} + +@media (max-width: 1024px) { + body { + padding-bottom: 4rem; + } + + .toc { + position: fixed; + inset: 0; + margin: 0; + max-width: none; + border-radius: 0; + z-index: 99; + transform: translateY(-110%); + transition: transform 0.25s ease; + overflow-y: auto; + } + + .toc.open { + transform: translateY(0); + } + + .toc ul { + padding-bottom: 4rem; + } + + .content { + margin: 0 auto 4rem; + width: min(1100px, calc(100% - 2.5rem)); + } + + .layout { + padding: 0 clamp(1.25rem, 8vw, 3rem) 3.5rem; + } +} + +@media (max-width: 640px) { + .hero__content { + max-width: 100%; + } + + .hero__cta { + flex-direction: column; + align-items: stretch; + } + + .card { + padding: 1.5rem; + } + + .card__sub { + padding: 1.1rem; + } + + pre { + font-size: 0.85rem; + } +} diff --git a/src/server/public/assets/js/main.js b/src/server/public/assets/js/main.js new file mode 100644 index 0000000..b458828 --- /dev/null +++ b/src/server/public/assets/js/main.js @@ -0,0 +1,70 @@ +const toggleButton = document.querySelector("#toggle-nav"); +const toc = document.querySelector("#toc"); + +const mobileOverlayClasses = [ + "fixed", + "inset-0", + "z-50", + "mx-auto", + "max-w-md", + "overflow-y-auto", + "bg-slate-950/95", + "p-6", + "backdrop-blur" +]; + +const openToc = () => { + if (!toc) return; + toc.classList.remove("hidden"); + if (window.innerWidth < 1024) { + toc.classList.add(...mobileOverlayClasses); + document.body.style.overflow = "hidden"; + } +}; + +const closeToc = () => { + if (!toc) return; + toc.classList.remove(...mobileOverlayClasses); + document.body.style.overflow = ""; + if (window.innerWidth < 1024) { + toc.classList.add("hidden"); + } +}; + +const syncTocToViewport = () => { + if (!toc) return; + if (window.innerWidth >= 1024) { + toc.classList.remove("hidden", ...mobileOverlayClasses); + document.body.style.overflow = ""; + } else { + toc.classList.add("hidden"); + toc.classList.remove(...mobileOverlayClasses); + document.body.style.overflow = ""; + } +}; + +if (toggleButton && toc) { + toggleButton.addEventListener("click", () => { + if (toc.classList.contains("hidden")) { + openToc(); + } else { + closeToc(); + } + }); + + toc.addEventListener("click", (event) => { + const target = event.target; + if (target instanceof HTMLAnchorElement) { + closeToc(); + } + }); +} + +window.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + closeToc(); + } +}); + +window.addEventListener("resize", syncTocToViewport); +window.addEventListener("load", syncTocToViewport); diff --git a/src/server/public/index.html b/src/server/public/index.html new file mode 100644 index 0000000..439a448 --- /dev/null +++ b/src/server/public/index.html @@ -0,0 +1,1234 @@ + + + + + + + Amayo Docs | Guía de Contenido + + + + + + + + + +
+
+
+
+
+
+
+
+

+ Amayo • Docs +

+

+ Documentación de Contenido Amayo +

+

+ Crea y gestiona items, enemigos, áreas, niveles, logros, misiones, cofres, + mutaciones y más usando los comandos del bot. Esta guía reúne los flujos + completos y plantillas listas para copiar. +

+
+ + Empezar ahora + + +
+
+ + Actualizado: 5 Oct 2025 + + + Compatible con DisplayComponents V2 + +
+
+
+ +
+ + +
+
+
+

Conceptos básicos

+

+ Toda pieza de contenido en Amayo se identifica mediante una key + única. Estas keys se usan en comandos, relaciones y validaciones. Usa un formato consistente como + categoria_nombre + (ej: item_iron_sword). +

+
+
+

Permisos necesarios

+
    +
  • Permiso de Discord Administrar Servidor.
  • +
  • O un rol Staff configurado para los comandos del bot.
  • +
+
+
+

Sistema de pesos

+

+ Al definir tablas de recompensas o aparición de enemigos, usa un campo weight. + Cuanto mayor sea el peso, mayor la probabilidad de ser seleccionado. +

+
{ "itemKey": "iron_ore", "weight": 10 }
+{ "itemKey": "gold_ore", "weight": 3 }
+
+
+
+ +
+

Items (EconomyItem)

+

+ Administra todo el inventario del juego. Usa !item-crear + para abrir el editor interactivo y completa cada pestaña antes de guardar. +

+
+
+

Comandos clave

+
    +
  • !item-crear <key> — Crear un item nuevo.
  • +
  • !item-editar <key> — Editar un item existente.
  • +
  • !items-lista [página] — Ver listado paginado.
  • +
  • !item-ver <key> — Ver detalles completos.
  • +
  • !item-eliminar <key> — Eliminar un item.
  • +
+
+
+

Campos del modal Base

+
    +
  • Nombre: Texto visible para jugadores.
  • +
  • Descripción: Lore o efectos.
  • +
  • Categoría: Agrupa items (ej. weapons).
  • +
  • Icon URL: Imagen opcional.
  • +
  • Stackable y Máx inventario: Usa true,10, + false,1 o deja el límite vacío para infinito. +
  • +
+
+
+
+

Props disponibles

+
+
+ Herramientas (tool) +
{
+  "tool": { "type": "pickaxe|rod|sword|bow|halberd|net", "tier": 1 }
+}
+

Define el tipo de actividad que habilita tu item. El campo tier controla los requisitos mínimos.

+
+
+ Durabilidad (breakable) +
{
+  "breakable": {
+    "enabled": true,
+    "maxDurability": 100,
+    "durabilityPerUse": 1
+  }
+}
+

Sólo funciona con items no apilables. Ajusta la pérdida de durabilidad por uso para balancear actividades.

+
+
+ Cofres (chest) +
{
+  "chest": {
+    "enabled": true,
+    "rewards": [ ... ],
+    "consumeOnOpen": true
+  }
+}
+

Permite definir loot tables internas, recompensas de monedas, items o roles.

+
+
+ Comida y pociones (food) +
{
+  "food": {
+    "healHp": 50,
+    "healPercent": 25,
+    "cooldownSeconds": 60
+  }
+}
+

Útil para pociones curativas o consumibles con cooldown.

+
+
+ Bonos de combate +
{
+  "damage": 10,
+  "defense": 5,
+  "maxHpBonus": 20
+}
+

Configura stats extra para armas, armaduras o capas.

+
+
+ Etiquetas y metadatos +

Usa el modal Tags para añadir etiquetas separadas por coma, como weapon,rare,crafteable. Sirven para filtrar o aplicar reglas.

+
+
+
+
+ +
+

Mobs (Enemigos)

+

+ Los enemigos definen los encuentros durante minijuegos y niveles de área. Se crean con + !mob-crear y usan stats y tablas de drop en formato JSON. +

+
+
+

Campos principales

+
    +
  • Base: Nombre y categoría opcional.
  • +
  • Stats: Define attack, + hp, defense, xpReward.
  • +
  • Drops: Incluye draws y una tabla con premios ponderados.
  • +
+
+
+

Ejemplo de configuración

+
{
+  "attack": 10,
+  "hp": 100,
+  "defense": 5,
+  "xpReward": 50
+}
+
{
+  "draws": 2,
+  "table": [
+    { "type": "coins", "amount": 50, "weight": 10 },
+    { "type": "item", "itemKey": "leather", "qty": 1, "weight": 5 }
+  ]
+}
+
+
+
+ Tip: + Usa !mobs-lista para auditar stats rápidamente y + !mob-ver <key> para revisar drops antes de activar un área. +
+
+ +
+

Áreas de juego (GameArea)

+

+ Las áreas definen dónde se desarrollan las actividades principales (minar, pescar, pelear, plantar). Cada área puede tener múltiples niveles configurables. +

+
+
+

Modal Base

+
    +
  • Nombre: Ej. Caverna de Hierro.
  • +
  • Tipo: MINE, + LAGOON, FIGHT o FARM. +
  • +
+
+
+

Modal Config (JSON)

+
{
+  "cooldownSeconds": 60,
+  "description": "Una mina profunda",
+  "icon": "⛏️"
+}
+

El ícono se mostrará en las tarjetas generadas por DisplayComponents.

+
+
+
+ Recuerda: + Si eliminas un área con !area-eliminar, revisa niveles asociados para evitar referencias rotas. +
+
+ +
+

Niveles de área (GameAreaLevel)

+

+ Cada nivel controla requisitos, mobs, recompensas y vigencia. Gestiona niveles con + !area-nivel <areaKey> <level>. +

+
+
+

Requisitos

+
{
+  "tool": {
+    "required": true,
+    "toolType": "pickaxe",
+    "minTier": 2,
+    "allowedKeys": ["iron_pickaxe", "diamond_pickaxe"]
+  }
+}
+

Sirve para validar herramientas necesarias. Combínalo con los tiers definidos en los items.

+
+
+

Recompensas

+
{
+  "draws": 3,
+  "table": [
+    { "type": "coins", "amount": 100, "weight": 10 },
+    { "type": "item", "itemKey": "iron_ore", "qty": 2, "weight": 5 }
+  ]
+}
+

Define múltiples extracciones de la tabla con pesos personalizados.

+
+
+
+
+

Mobs

+
{
+  "mobPool": {
+    "draws": 2,
+    "table": [
+      { "mobKey": "goblin", "weight": 10 },
+      { "mobKey": "troll", "weight": 3 }
+    ]
+  }
+}
+
+
+

Ventana

+
{
+  "window": {
+    "from": "2025-01-01T00:00:00Z",
+    "to": "2025-01-31T23:59:59Z"
+  }
+}
+
+
+
+ +
+

Ofertas de tienda (ShopOffer)

+

+ Usa !offer-crear para lanzar nuevas ofertas con stock limitado, + precios compuestos y ventanas temporales. +

+
+
+

Precio (JSON)

+
{
+  "coins": 100,
+  "items": [
+    { "itemKey": "iron_ore", "qty": 5 },
+    { "itemKey": "wood", "qty": 10 }
+  ]
+}
+
+
+

Límites y ventana

+
    +
  • Límite por usuario: Máximo por jugador.
  • +
  • Stock global: Total disponible.
  • +
  • Ventana: Fechas ISO de inicio y fin.
  • +
+
+
+
+ +
+

Logros

+

+ Motiva a los jugadores con hitos permanentes. Crea logros con + !logro-crear y configúralos usando el editor DisplayComponents. +

+
+
+

Requisitos comunes

+
    +
  • collect_items: Recolectar items específicos.
  • +
  • complete_missions: Completar misiones listadas.
  • +
  • reach_level: Alcanzar cierto nivel o racha.
  • +
  • stat_value: Llegar a un valor en estadísticas.
  • +
+
+
+

Recompensas posibles

+
    +
  • Monedas
  • +
  • Items entregados automáticamente
  • +
  • Roles (usa ID de Discord)
  • +
  • Puntos de logro
  • +
+
+
+
+ Nota: + Usa !logros-lista para auditar logros y + !logro-ver <key> para validar estructura antes de publicarlos. +
+
+ +
+

Misiones

+

+ Las misiones permiten objetivos diarios, semanales o repetibles. Adminístralas con + !mision-crear y + !misiones-lista. +

+
+
+

Tipos de misión

+
    +
  • daily: Reinicia cada día.
  • +
  • weekly: Reinicia cada semana.
  • +
  • one_time: Se completa una vez.
  • +
  • repeatable: Puede repetirse sin límite.
  • +
+
+
+

Requisitos combinables

+
    +
  • Consumir items o recursos.
  • +
  • Completar minijuegos específicos.
  • +
  • Derrotar mobs concretos.
  • +
  • Lograr cantidades de monedas.
  • +
+
+
+
+ Tip: + Aprovecha la ventana de disponibilidad para crear eventos temáticos limitados. +
+
+ +
+

Cofres y recompensas

+

+ Configura cofres usando props chest en los items y define tablas de recompensas con pesos. +

+
+
+

Recompensas soportadas

+
    +
  • Monedas (coins)
  • +
  • Items (usa itemKey y qty)
  • +
  • Roles de Discord (roleId)
  • +
+
+
+

Consejos

+
    +
  • Usa consumeOnOpen para cofres desechables.
  • +
  • Combina cofres con logros y eventos para mejores recompensas.
  • +
  • Define varias entradas con pesos distintos para crear rarezas.
  • +
+
+
+
+ +
+

Crafteos

+

+ Gestiona recetas desde la base de datos o crea comandos personalizados. El servicio de economía incluye + craftByProductKey para validar materiales y entregar productos. +

+
    +
  • Define recetas en Prisma con entradas y productos.
  • +
  • Usa misiones o eventos para desbloquear recetas temporales combinando props y tags.
  • +
  • Considera usar craftingOnly: true en items que no se consiguen por drops.
  • +
+
+ +
+

Mutaciones

+

+ Las mutaciones permiten modificar items existentes con efectos adicionales (ej. reforjar armas). Usa + findMutationByKey y + applyMutationToInventory desde el servicio de economía. +

+
+
+

Políticas

+
{
+  "mutationPolicy": {
+    "allowedKeys": ["fire_upgrade", "ice_upgrade"],
+    "deniedKeys": ["cursed_upgrade"]
+  }
+}
+
+
+

Sugerencias

+
    +
  • Crea mutaciones exclusivas por eventos.
  • +
  • Combínalas con logros o misiones épicas.
  • +
  • Controla conflictos usando deniedKeys.
  • +
+
+
+
+ +
+

Pociones y consumibles

+

+ Usa props food para crear pociones curativas, boosters temporales o consumibles con cooldown. +

+
+
+

Ejemplo de poción

+
{
+  "food": {
+    "healHp": 75,
+    "healPercent": 15,
+    "cooldownKey": "healing_potion",
+    "cooldownSeconds": 45
+  }
+}
+
+
+

Buenas prácticas

+
    +
  • Usa cooldownKey para compartir cooldown.
  • +
  • Balancea healHp y healPercent para distintos niveles.
  • +
  • Combina con logros para recompensar uso estratégico.
  • +
+
+
+
+ +
+

Herramientas y durabilidad

+

+ La durabilidad se administra a través de la combinación de props tool + y breakable. Para que un item pierda durabilidad, debe ser + no apilable (stackable=false). +

+
    +
  • La función reduceToolDurability descuenta durabilityPerUse tras cada minijuego.
  • +
  • Cuando la durabilidad llega a 0, el item se elimina del inventario.
  • +
  • Si breakable.enabled es false, la herramienta es indestructible.
  • +
  • Usa tiers para bloquear áreas avanzadas. Ejemplo: Un área puede requerir una herramienta pickaxe con tier >= 2.
  • +
+
+ Importante: + Después de cambiar items apilables a no apilables, recrea el item en los inventarios existentes para evitar stacks rotos. +
+
+ +
+

Servicios del sistema

+
+
+

Economy Service

+
    +
  • findItemByKey y addItemByKey
  • +
  • consumeItemByKey y getInventoryEntry
  • +
  • craftByProductKey y buyFromOffer
  • +
  • findMutationByKey y applyMutationToInventory
  • +
+
+
+

Minigames Service

+
    +
  • runMinigame para ejecutar cualquier actividad.
  • +
  • runMining y runFishing como atajos.
  • +
  • Valida cooldowns, requisitos de herramientas y entrega recompensas.
  • +
  • Reduce durabilidad automáticamente cuando corresponde.
  • +
+
+
+
+ +
+

Preguntas frecuentes

+
+
+ ¿Qué pasa si olvido definir stackable? +

+ Por defecto los items son apilables. Si tu herramienta pierde durabilidad, asegúrate de marcarla como no apilable en el modal Base. +

+
+
+ ¿Cómo pruebo mis configuraciones? +

+ Usa comandos de prueba en un servidor privado con el bot y confirma con + !player, + !stats y !inventario. +

+
+
+ ¿Puedo clonar contenido entre servidores? +

+ Sí. Los items globales están disponibles en todos los servidores; los locales se limitan a su guild. Usa las herramientas de exportación de Prisma si necesitas migraciones masivas. +

+
+
+ ¿Cómo despliego esta documentación? +

+ Consulta las instrucciones en server/README.md para publicar en Heroku como app independiente. +

+
+
+
+
+
+
+ + +
+ + + + + + + Amayo Docs | Guía de Contenido + + + + + + + +
+
+

Documentación de Contenido Amayo

+

+ Crea y gestiona items, enemigos, áreas, niveles, logros, misiones, cofres, + mutaciones y más usando los comandos del bot. Esta guía reúne los flujos + completos y plantillas listas para copiar. +

+
+ Empezar ahora + +
+
+
+ Actualizado: 5 Oct 2025 + Compatible con DisplayComponents V2 +
+
+ +
+ + +
+
+

Conceptos básicos

+

+ Toda pieza de contenido en Amayo se identifica mediante una key + única. Estas keys se usan en comandos, relaciones y validaciones. Usa un + formato consistente como categoria_nombre (ej: + item_iron_sword). +

+
+
+

Permisos necesarios

+
    +
  • Permiso de Discord Administrar Servidor.
  • +
  • O bien un rol Staff configurado para los comandos del bot.
  • +
+
+
+

Sistema de pesos

+

+ Al definir tablas de recompensas o aparición de enemigos, usa un campo + weight. Cuanto mayor sea el peso, mayor la probabilidad de ser + seleccionado. +

+
{ "itemKey": "iron_ore", "weight": 10 }
+{ "itemKey": "gold_ore", "weight": 3 }
+
+
+
+ +
+

Items (EconomyItem)

+

+ Administra todo el inventario del juego. Usa !item-crear para abrir + el editor interactivo y completa cada pestaña antes de guardar. +

+
+
+

Comandos clave

+
    +
  • !item-crear <key> — Crear un item nuevo.
  • +
  • !item-editar <key> — Editar un item existente.
  • +
  • !items-lista [página] — Ver listado paginado.
  • +
  • !item-ver <key> — Ver detalles completos.
  • +
  • !item-eliminar <key> — Eliminar un item.
  • +
+
+
+

Campos del modal Base

+
    +
  • Nombre: Texto visible para jugadores.
  • +
  • Descripción: Lore o efectos.
  • +
  • Categoría: Agrupa items (ej. weapons).
  • +
  • Icon URL: Imagen opcional.
  • +
  • + Stackable y Máx inventario: Usa + true,10, false,1 o deja el límite vacío para + infinito. +
  • +
+
+
+
+

Props disponibles

+
+
+ Herramientas (tool) +
{
+  "tool": { "type": "pickaxe|rod|sword|bow|halberd|net", "tier": 1 }
+}
+

+ Define el tipo de actividad que habilita tu item. El campo + tier controla los requisitos mínimos de áreas y minijuegos. +

+
+
+ Durabilidad (breakable) +
{
+  "breakable": {
+    "enabled": true,
+    "maxDurability": 100,
+    "durabilityPerUse": 1
+  }
+}
+

+ Sólo funciona con items no apilables. Ajusta la pérdida de + durabilidad por uso para balancear actividades. +

+
+
+ Cofres (chest) +
{
+  "chest": {
+    "enabled": true,
+    "rewards": [ ... ],
+    "consumeOnOpen": true
+  }
+}
+

+ Permite definir loot tables internas, recompensas de monedas, items o roles. +

+
+
+ Comida y pociones (food) +
{
+  "food": {
+    "healHp": 50,
+    "healPercent": 25,
+    "cooldownSeconds": 60
+  }
+}
+

Útil para pociones curativas o consumibles con cooldown.

+
+
+ Bonos de combate +
{
+  "damage": 10,
+  "defense": 5,
+  "maxHpBonus": 20
+}
+

Configura stats extra para armas, armaduras o capas.

+
+
+ Etiquetas y metadatos +

+ Usa el modal Tags para añadir etiquetas separadas por coma, como + weapon,rare,crafteable. Sirven para filtrar o aplicar reglas. +

+
+
+
+
+ +
+

Mobs (Enemigos)

+

+ Los enemigos definen los encuentros durante minijuegos y niveles de área. Se + crean con !mob-crear y usan stats y tablas de drop en formato JSON. +

+
+
+

Campos principales

+
    +
  • Base: Nombre y categoría opcional.
  • +
  • + Stats: Define attack, hp, + defense, xpReward. +
  • +
  • + Drops: Incluye draws y una tabla con premios + ponderados. +
  • +
+
+
+

Ejemplo de configuración

+
{
+  "attack": 10,
+  "hp": 100,
+  "defense": 5,
+  "xpReward": 50
+}
+
{
+  "draws": 2,
+  "table": [
+    { "type": "coins", "amount": 50, "weight": 10 },
+    { "type": "item", "itemKey": "leather", "qty": 1, "weight": 5 }
+  ]
+}
+
+
+
+ Tip: Usa !mobs-lista para auditar stats rápidamente y + !mob-ver <key> para revisar drops antes de activar un área. +
+
+ +
+

Áreas de juego (GameArea)

+

+ Las áreas definen dónde se desarrollan las actividades principales (minar, + pescar, pelear, plantar). Cada área puede tener múltiples niveles configurables. +

+
+
+

Modal Base

+
    +
  • Nombre: Ej. Caverna de Hierro.
  • +
  • Tipo: MINE, LAGOON, + FIGHT o FARM.
  • +
+
+
+

Modal Config (JSON)

+
{
+  "cooldownSeconds": 60,
+  "description": "Una mina profunda",
+  "icon": "⛏️"
+}
+

El ícono se mostrará en las tarjetas generadas por DisplayComponents.

+
+
+
+ Recuerda: Si eliminas un área con !area-eliminar, + revisa niveles asociados para evitar referencias rotas. +
+
+ +
+

Niveles de área (GameAreaLevel)

+

+ Cada nivel controla requisitos, mobs, recompensas y vigencia. Gestiona niveles con + !area-nivel <areaKey> <level>. +

+
+
+

Requisitos

+
{
+  "tool": {
+    "required": true,
+    "toolType": "pickaxe",
+    "minTier": 2,
+    "allowedKeys": ["iron_pickaxe", "diamond_pickaxe"]
+  }
+}
+

+ Sirve para validar herramientas necesarias. Combínalo con los tiers definidos + en los items. +

+
+
+

Recompensas

+
{
+  "draws": 3,
+  "table": [
+    { "type": "coins", "amount": 100, "weight": 10 },
+    { "type": "item", "itemKey": "iron_ore", "qty": 2, "weight": 5 }
+  ]
+}
+

Define múltiples extracciones de la tabla con pesos personalizados.

+
+
+
+

Mobs y ventana

+
{
+  "mobPool": {
+    "draws": 2,
+    "table": [
+      { "mobKey": "goblin", "weight": 10 },
+      { "mobKey": "troll", "weight": 3 }
+    ]
+  }
+}
+
{
+  "window": {
+    "from": "2025-01-01T00:00:00Z",
+    "to": "2025-01-31T23:59:59Z"
+  }
+}
+
+
+ +
+

Ofertas de tienda (ShopOffer)

+

+ Usa !offer-crear para lanzar nuevas ofertas con stock limitado, + precios compuestos y ventanas temporales. +

+
+
+

Precio (JSON)

+
{
+  "coins": 100,
+  "items": [
+    { "itemKey": "iron_ore", "qty": 5 },
+    { "itemKey": "wood", "qty": 10 }
+  ]
+}
+
+
+

Límites y ventana

+
    +
  • Límite por usuario: Máximo por jugador.
  • +
  • Stock global: Total disponible.
  • +
  • Ventana: Fechas ISO de inicio y fin.
  • +
+
+
+
+ +
+

Logros

+

+ Motiva a los jugadores con hitos permanentes. Crea logros con + !logro-crear y configúralos usando el editor DisplayComponents. +

+
+
+

Requisitos comunes

+
    +
  • collect_items: Recolectar items específicos.
  • +
  • complete_missions: Completar misiones listadas.
  • +
  • reach_level: Alcanzar cierto nivel o racha.
  • +
  • stat_value: Llegar a un valor en estadísticas.
  • +
+
+
+

Recompensas posibles

+
    +
  • Monedas
  • +
  • Items entregados automáticamente
  • +
  • Roles (usa ID de Discord)
  • +
  • Puntos de logro
  • +
+
+
+
+ Nota: Usa !logros-lista para auditar logros y + !logro-ver <key> para validar estructura antes de publicarlos. +
+
+ +
+

Misiones

+

+ Las misiones permiten objetivos diarios, semanales o repetibles. Adminístralas con + !mision-crear y !misiones-lista. +

+
+
+

Tipos de misión

+
    +
  • daily: Reinicia cada día.
  • +
  • weekly: Reinicia cada semana.
  • +
  • one_time: Se completa una vez.
  • +
  • repeatable: Puede repetirse sin límite.
  • +
+
+
+

Requisitos combinables

+
    +
  • Consumir items o recursos.
  • +
  • Completar minijuegos específicos.
  • +
  • Derrotar mobs concretos.
  • +
  • Lograr cantidades de monedas.
  • +
+
+
+
+ Tip: Aprovecha la ventana de disponibilidad para crear eventos + temáticos limitados. +
+
+ +
+

Cofres y recompensas

+

+ Configura cofres usando props chest en los items y define tablas de + recompensas con pesos. Ideal para loot boxes, recompensas diarias o drops raros. +

+
+
+

Recompensas soportadas

+
    +
  • Monedas (coins)
  • +
  • Items (usa itemKey y qty)
  • +
  • Roles de Discord (roleId)
  • +
+
+
+

Consejos

+
    +
  • Usa consumeOnOpen para cofres desechables.
  • +
  • Combina cofres con logros y eventos para mejores recompensas.
  • +
  • Define varias entradas con pesos distintos para crear rarezas.
  • +
+
+
+
+ +
+

Crafteos

+

+ Gestiona recetas desde la base de datos o crea comandos personalizados. El servicio + de economía incluye craftByProductKey para validar materiales y + entregar productos. +

+
    +
  • Define recetas en Prisma con entradas y productos.
  • +
  • + Usa misiones o eventos para desbloquear recetas temporales combinando props y + tags. +
  • +
  • + Considera usar craftingOnly: true en items que no se consiguen por + drops. +
  • +
+
+ +
+

Mutaciones

+

+ Las mutaciones permiten modificar items existentes con efectos adicionales (ej. + reforjar armas). Usa findMutationByKey y + applyMutationToInventory desde el servicio de economía. +

+
+
+

Políticas

+
{
+  "mutationPolicy": {
+    "allowedKeys": ["fire_upgrade", "ice_upgrade"],
+    "deniedKeys": ["cursed_upgrade"]
+  }
+}
+
+
+

Sugerencias

+
    +
  • Crea mutaciones exclusivas por eventos.
  • +
  • Combínalas con logros o misiones épicas.
  • +
  • Controla conflictos usando deniedKeys.
  • +
+
+
+
+ +
+

Pociones y consumibles

+

+ Usa props food para crear pociones curativas, boosters temporales o + consumibles con cooldown. Complementa con misiones diarias o drops específicos. +

+
+
+

Ejemplo de poción

+
{
+  "food": {
+    "healHp": 75,
+    "healPercent": 15,
+    "cooldownKey": "healing_potion",
+    "cooldownSeconds": 45
+  }
+}
+
+
+

Buenas prácticas

+
    +
  • Usa cooldownKey para compartir cooldown entre pociones similares.
  • +
  • Balancea healHp y healPercent para distintos niveles de jugador.
  • +
  • Combina con logros para recompensar uso estratégico.
  • +
+
+
+
+ +
+

Herramientas y durabilidad

+

+ La durabilidad se administra a través de la combinación de props tool + y breakable. Para que un item pierda durabilidad, debe ser + no apilable (stackable=false en el modal Base). +

+
    +
  • La función reduceToolDurability descuenta + durabilityPerUse tras cada minijuego.
  • +
  • Cuando la durabilidad llega a 0, el item se elimina del inventario.
  • +
  • Si breakable.enabled es false, la herramienta es indestructible.
  • +
  • + Usa tiers para bloquear áreas avanzadas. Ejemplo: Un área puede requerir una + herramienta pickaxe con tier >= 2. +
  • +
+
+ Importante: Después de cambiar items apilables a no apilables, + recrea el item en los inventarios existentes para evitar stacks rotos. +
+
+ +
+

Servicios del sistema

+
+
+

Economy Service

+
    +
  • findItemByKey y addItemByKey
  • +
  • consumeItemByKey y getInventoryEntry
  • +
  • craftByProductKey y buyFromOffer
  • +
  • findMutationByKey y applyMutationToInventory
  • +
+
+
+

Minigames Service

+
    +
  • runMinigame para ejecutar cualquier actividad.
  • +
  • runMining y runFishing como atajos.
  • +
  • Valida cooldowns, requisitos de herramientas y entrega recompensas.
  • +
  • Reduce durabilidad automáticamente cuando corresponde.
  • +
+
+
+
+ +
+

Preguntas frecuentes

+
+ ¿Qué pasa si olvido definir stackable? +

Por defecto los items son apilables. Si tu herramienta pierde durabilidad, asegúrate de marcarla como no apilable en el modal Base.

+
+
+ ¿Cómo pruebo mis configuraciones? +

Usa comandos de prueba en un servidor privado con el bot y confirma con !player, !stats y !inventario.

+
+
+ ¿Puedo clonar contenido entre servidores? +

Sí. Los items globales están disponibles en todos los servidores; los locales se limitan a su guild. Usa las herramientas de exportación de Prisma si necesitas migraciones masivas.

+
+
+ ¿Cómo despliego esta documentación? +

Consulta las instrucciones en server/README.md para publicar en Heroku como app independiente.

+
+
+
+
+ + + + + + diff --git a/src/server/server.ts b/src/server/server.ts new file mode 100644 index 0000000..e4abfb0 --- /dev/null +++ b/src/server/server.ts @@ -0,0 +1,105 @@ +import { createServer, IncomingMessage, ServerResponse } from "node:http"; +import { promises as fs } from "node:fs"; +import path from "node:path"; + +const publicDir = path.join(__dirname, "public"); + +const MIME_TYPES: Record = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".svg": "image/svg+xml", + ".webp": "image/webp", + ".ico": "image/x-icon", + ".woff": "font/woff", + ".woff2": "font/woff2", +}; + +const PORT = Number(process.env.PORT) || 3000; + +const resolvePath = (pathname: string): string => { + const decoded = decodeURIComponent(pathname); + let target = decoded; + + if (target.endsWith("/")) { + target = `${target}index.html`; + } + + if (!path.extname(target)) { + target = `${target}.html`; + } + + return path.join(publicDir, target); +}; + +const sendResponse = async ( + res: ServerResponse, + filePath: string, + statusCode = 200 +): Promise => { + const extension = path.extname(filePath).toLowerCase(); + const mimeType = MIME_TYPES[extension] || "application/octet-stream"; + const cacheControl = extension.match(/\.(?:html)$/) + ? "no-cache" + : "public, max-age=86400, immutable"; + + const data = await fs.readFile(filePath); + res.writeHead(statusCode, { + "Content-Type": mimeType, + "Cache-Control": cacheControl, + }); + res.end(data); +}; + +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"}`); + 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"); + } +});