From 9c20ca093035db0ef22c140422ac053216344f84 Mon Sep 17 00:00:00 2001 From: Shnimlz Date: Tue, 25 Nov 2025 02:28:30 -0600 Subject: [PATCH] delete scripts/*.md added .backup to gitignore --- .gitignore | 1 + convert-modals-v14-v2.ps1 | 25 - convert-modals-v14-v3.ps1 | 31 - convert-modals-v14-v4.ps1 | 106 -- convert-modals-v14.ps1 | 58 - scripts/check_brackets.js | 51 - scripts/check_nav_render.js | 73 - scripts/cleanInvalidMobs.ts | 54 - scripts/collab-tests/dashboard/README.md | 52 - .../dashboard/check_dashboard_render.js | 35 - .../dashboard/check_roles_render.js | 42 - scripts/collab-tests/dashboard/run.sh | 11 - scripts/debugFeatureFlags.ts | 88 -- scripts/debugInventory.ts | 113 -- scripts/findMobDependencies.ts | 80 - scripts/fullServerSetup.ts | 393 ----- scripts/migrateStackableToInstanced.ts | 253 --- scripts/mobAdminTest.ts | 41 - scripts/removeInvalidMobsWithDeps.ts | 72 - scripts/setupFeatureFlags.ts | 173 --- scripts/setupGuildCacheCollection.js | 159 -- scripts/smokeQuestsAchievements.ts | 99 -- scripts/testCreateFlag.ts | 43 - scripts/testDiscordCommandFlow.ts | 80 - scripts/testMobData.ts | 18 - scripts/testMobUnit.ts | 54 - scripts/testRewardMods.ts | 66 - scripts/test_render.js | 16 - src/.backup/admin/areaEliminar.ts | 104 -- src/.backup/admin/areasLista.ts | 117 -- src/.backup/admin/debugInv.ts | 103 -- src/.backup/admin/fixDurability.ts | 96 -- src/.backup/admin/itemEliminar.ts | 64 - src/.backup/admin/itemVer.ts | 77 - src/.backup/admin/itemsLista.ts | 146 -- src/.backup/admin/logroCrear.ts | 365 ----- src/.backup/admin/logroEliminar.ts | 61 - src/.backup/admin/logroVer.ts | 89 -- src/.backup/admin/logrosLista.ts | 134 -- src/.backup/admin/misionCrear.ts | 373 ----- src/.backup/admin/misionEliminar.ts | 58 - src/.backup/admin/misionVer.ts | 156 -- src/.backup/admin/misionesLista.ts | 143 -- src/.backup/admin/mobEliminar.ts | 47 - src/.backup/admin/mobsLista.ts | 126 -- src/.backup/admin/resetInventory.ts | 185 --- src/.backup/areaCreate.ts.backup2 | 101 -- src/.backup/areaEdit.ts.backup2 | 101 -- src/.backup/createDisplayComponent.backup.ts | 618 -------- src/.backup/createDisplayComponent.ts | 1052 ------------- src/.backup/createEmbed.backup.ts | 494 ------ src/.backup/editEmbed.backup.ts | 276 ---- src/.backup/game/_helpers.ts | 665 -------- src/.backup/game/abrir.ts | 82 - src/.backup/game/areaCreate.ts | 463 ------ src/.backup/game/areaEdit.ts | 256 --- src/.backup/game/areaNivel.ts | 336 ---- src/.backup/game/combatehistorial.ts | 66 - src/.backup/game/comer.ts | 32 - src/.backup/game/comprar.ts | 26 - src/.backup/game/cooldowns.ts | 125 -- src/.backup/game/craftear.ts | 48 - src/.backup/game/deathlog.ts | 49 - src/.backup/game/durabilidad.ts | 98 -- src/.backup/game/effects.ts | 104 -- src/.backup/game/encantar.ts | 33 - src/.backup/game/equipar.ts | 273 ---- src/.backup/game/fundir.ts | 40 - src/.backup/game/fundirReclamar.ts | 28 - src/.backup/game/inventario.ts | 240 --- src/.backup/game/itemCreate.ts | 791 ---------- src/.backup/game/itemEdit.ts | 825 ---------- src/.backup/game/logros.ts | 117 -- src/.backup/game/mina.ts | 228 --- src/.backup/game/misionReclamar.ts | 89 -- src/.backup/game/misiones.ts | 157 -- src/.backup/game/mobCreate.ts | 481 ------ src/.backup/game/mobDelete.ts | 178 --- src/.backup/game/mobEdit.ts | 439 ------ src/.backup/game/monedas.ts | 45 - src/.backup/game/offerCreate.ts | 129 -- src/.backup/game/offerEdit.ts | 339 ---- src/.backup/game/pelear.ts | 256 --- src/.backup/game/pescar.ts | 222 --- src/.backup/game/plantar.ts | 119 -- src/.backup/game/player.ts | 229 --- src/.backup/game/racha.ts | 119 -- src/.backup/game/setup.ts | 72 - src/.backup/game/stats.ts | 110 -- src/.backup/game/tienda.ts | 594 ------- src/.backup/game/toolbreaks.ts | 54 - src/.backup/game/toolinfo.ts | 90 -- .../game_core/game/achievements/seed.ts | 221 --- .../game_core/game/achievements/service.ts | 248 --- .../game_core/game/combat/attacksWorker.ts | 90 -- .../game_core/game/combat/equipmentService.ts | 181 --- .../game/combat/statusEffectsService.ts | 91 -- .../game_core/game/consumables/service.ts | 40 - .../game_core/game/consumables/utils.ts | 16 - .../game_core/game/cooldowns/service.ts | 24 - .../game_core/game/core/userService.ts | 89 -- src/.backup/game_core/game/core/utils.ts | 55 - .../game_core/game/economy/seedPurgePotion.ts | 35 - src/.backup/game_core/game/economy/service.ts | 494 ------ src/.backup/game_core/game/economy/types.ts | 133 -- src/.backup/game_core/game/lib/rpgFormat.ts | 131 -- .../game_core/game/lib/toolBreakLog.ts | 33 - .../game_core/game/minigames/demoRun.ts | 24 - src/.backup/game_core/game/minigames/seed.ts | 674 -------- .../game_core/game/minigames/service.ts | 1165 -------------- .../game_core/game/minigames/testHelpers.ts | 32 - src/.backup/game_core/game/minigames/types.ts | 120 -- src/.backup/game_core/game/mobs/README.md | 16 - src/.backup/game_core/game/mobs/admin.ts | 207 --- src/.backup/game_core/game/mobs/mobData.ts | 282 ---- .../game_core/game/mutations/service.ts | 38 - src/.backup/game_core/game/quests/service.ts | 391 ----- src/.backup/game_core/game/rewards/service.ts | 95 -- .../game_core/game/smelting/service.ts | 81 - src/.backup/game_core/game/stats/service.ts | 177 --- src/.backup/game_core/game/stats/types.ts | 41 - src/.backup/game_core/game/streaks/service.ts | 174 --- src/.backup/itemCreate.ts.backup2 | 227 --- src/.backup/itemEdit.ts.backup2 | 143 -- src/.backup/listChannels.backup.ts.txt | 346 ----- src/.backup/mobCreate.ts.backup2 | 95 -- src/.backup/mobEdit.ts.backup2 | 100 -- src/.backup/offerCreate.ts.backup2 | 129 -- src/.backup/offerEdit.ts.backup2 | 148 -- src/.backup/player.ts.backup | 215 --- .../prompts/discord-api-expert.prompt.md | 15 - src/.backup/prompts/discord-helper.prompt.md | 14 - .../prompts/resource-checker.prompt.md | 41 - src/.backup/prompts/ts-validation.prompt.md | 7 - src/.backup/views.backup/index.ejs | 108 -- src/.backup/views.backup/layouts/layout.ejs | 106 -- src/.backup/views.backup/pages/index.ejs | 99 -- src/.backup/views.backup/partials/navbar.ejs | 16 - .../views.backup/partials/rightSidebar.ejs | 32 - .../views.backup/partials/sections/admin.ejs | 4 - .../partials/sections/alianzas.ejs | 4 - .../views.backup/partials/sections/cofres.ejs | 29 - .../partials/sections/comandos-basicos.ejs | 26 - .../partials/sections/configuracion.ejs | 4 - .../partials/sections/consumibles.ejs | 28 - .../partials/sections/crafteo.ejs | 77 - .../partials/sections/creacion-contenido.ejs | 156 -- .../partials/sections/economia.ejs | 31 - .../partials/sections/ejemplos-avanzados.ejs | 124 -- .../partials/sections/ejemplos-basicos.ejs | 47 - .../partials/sections/encantamientos.ejs | 26 - .../partials/sections/estadisticas.ejs | 4 - .../views.backup/partials/sections/faq.ejs | 67 - .../partials/sections/fundicion.ejs | 24 - .../views.backup/partials/sections/ia.ejs | 4 - .../partials/sections/inventario-equipo.ejs | 48 - .../views.backup/partials/sections/logros.ejs | 28 - .../partials/sections/minijuegos.ejs | 57 - .../partials/sections/misiones.ejs | 34 - .../partials/sections/primeros-pasos.ejs | 39 - .../views.backup/partials/sections/racha.ejs | 22 - .../partials/sections/recordatorios.ejs | 4 - .../partials/sections/sistema-juego.ejs | 36 - .../views.backup/partials/sections/tienda.ejs | 28 - .../views.backup/partials/sections/tips.ejs | 4 - src/.backup/views.backup/partials/toc.ejs | 32 - .../alliaces/createDisplayComponent.ts.backup | 1381 ----------------- src/test_help_logic.ts | 30 - test/.validate_dashboard_items_syntax.js | 84 - test/example.ts.txt | 145 -- test/examples/featureFlagsCommands.ts | 359 ----- test/invalid_mobs_backup.json | 131 -- test/mob.test.ts | 46 - test/scheduled_mob_attack_backup.json | 42 - test/tmp_acorn.js | 20 - test/tmp_check.js | 43 - test/tmp_find_parse_error.js | 11 - test/tmp_line_parse.js | 10 - test/tmp_print_script.js | 11 - test/tmp_token_balance.js | 17 - test/tmp_token_trace.js | 20 - test/tmp_tokens_inspect.js | 8 - test/unit/questsAchievements.unit.ts | 103 -- test/unit/rewardMods.test.ts | 59 - test/unit/rewardMods.unit.ts | 57 - 185 files changed, 1 insertion(+), 26822 deletions(-) delete mode 100644 convert-modals-v14-v2.ps1 delete mode 100644 convert-modals-v14-v3.ps1 delete mode 100644 convert-modals-v14-v4.ps1 delete mode 100644 convert-modals-v14.ps1 delete mode 100644 scripts/check_brackets.js delete mode 100644 scripts/check_nav_render.js delete mode 100644 scripts/cleanInvalidMobs.ts delete mode 100644 scripts/collab-tests/dashboard/README.md delete mode 100644 scripts/collab-tests/dashboard/check_dashboard_render.js delete mode 100644 scripts/collab-tests/dashboard/check_roles_render.js delete mode 100755 scripts/collab-tests/dashboard/run.sh delete mode 100644 scripts/debugFeatureFlags.ts delete mode 100644 scripts/debugInventory.ts delete mode 100644 scripts/findMobDependencies.ts delete mode 100644 scripts/fullServerSetup.ts delete mode 100644 scripts/migrateStackableToInstanced.ts delete mode 100644 scripts/mobAdminTest.ts delete mode 100644 scripts/removeInvalidMobsWithDeps.ts delete mode 100644 scripts/setupFeatureFlags.ts delete mode 100644 scripts/setupGuildCacheCollection.js delete mode 100644 scripts/smokeQuestsAchievements.ts delete mode 100644 scripts/testCreateFlag.ts delete mode 100644 scripts/testDiscordCommandFlow.ts delete mode 100644 scripts/testMobData.ts delete mode 100644 scripts/testMobUnit.ts delete mode 100644 scripts/testRewardMods.ts delete mode 100644 scripts/test_render.js delete mode 100644 src/.backup/admin/areaEliminar.ts delete mode 100644 src/.backup/admin/areasLista.ts delete mode 100644 src/.backup/admin/debugInv.ts delete mode 100644 src/.backup/admin/fixDurability.ts delete mode 100644 src/.backup/admin/itemEliminar.ts delete mode 100644 src/.backup/admin/itemVer.ts delete mode 100644 src/.backup/admin/itemsLista.ts delete mode 100644 src/.backup/admin/logroCrear.ts delete mode 100644 src/.backup/admin/logroEliminar.ts delete mode 100644 src/.backup/admin/logroVer.ts delete mode 100644 src/.backup/admin/logrosLista.ts delete mode 100644 src/.backup/admin/misionCrear.ts delete mode 100644 src/.backup/admin/misionEliminar.ts delete mode 100644 src/.backup/admin/misionVer.ts delete mode 100644 src/.backup/admin/misionesLista.ts delete mode 100644 src/.backup/admin/mobEliminar.ts delete mode 100644 src/.backup/admin/mobsLista.ts delete mode 100644 src/.backup/admin/resetInventory.ts delete mode 100644 src/.backup/areaCreate.ts.backup2 delete mode 100644 src/.backup/areaEdit.ts.backup2 delete mode 100644 src/.backup/createDisplayComponent.backup.ts delete mode 100644 src/.backup/createDisplayComponent.ts delete mode 100644 src/.backup/createEmbed.backup.ts delete mode 100644 src/.backup/editEmbed.backup.ts delete mode 100644 src/.backup/game/_helpers.ts delete mode 100644 src/.backup/game/abrir.ts delete mode 100644 src/.backup/game/areaCreate.ts delete mode 100644 src/.backup/game/areaEdit.ts delete mode 100644 src/.backup/game/areaNivel.ts delete mode 100644 src/.backup/game/combatehistorial.ts delete mode 100644 src/.backup/game/comer.ts delete mode 100644 src/.backup/game/comprar.ts delete mode 100644 src/.backup/game/cooldowns.ts delete mode 100644 src/.backup/game/craftear.ts delete mode 100644 src/.backup/game/deathlog.ts delete mode 100644 src/.backup/game/durabilidad.ts delete mode 100644 src/.backup/game/effects.ts delete mode 100644 src/.backup/game/encantar.ts delete mode 100644 src/.backup/game/equipar.ts delete mode 100644 src/.backup/game/fundir.ts delete mode 100644 src/.backup/game/fundirReclamar.ts delete mode 100644 src/.backup/game/inventario.ts delete mode 100644 src/.backup/game/itemCreate.ts delete mode 100644 src/.backup/game/itemEdit.ts delete mode 100644 src/.backup/game/logros.ts delete mode 100644 src/.backup/game/mina.ts delete mode 100644 src/.backup/game/misionReclamar.ts delete mode 100644 src/.backup/game/misiones.ts delete mode 100644 src/.backup/game/mobCreate.ts delete mode 100644 src/.backup/game/mobDelete.ts delete mode 100644 src/.backup/game/mobEdit.ts delete mode 100644 src/.backup/game/monedas.ts delete mode 100644 src/.backup/game/offerCreate.ts delete mode 100644 src/.backup/game/offerEdit.ts delete mode 100644 src/.backup/game/pelear.ts delete mode 100644 src/.backup/game/pescar.ts delete mode 100644 src/.backup/game/plantar.ts delete mode 100644 src/.backup/game/player.ts delete mode 100644 src/.backup/game/racha.ts delete mode 100644 src/.backup/game/setup.ts delete mode 100644 src/.backup/game/stats.ts delete mode 100644 src/.backup/game/tienda.ts delete mode 100644 src/.backup/game/toolbreaks.ts delete mode 100644 src/.backup/game/toolinfo.ts delete mode 100644 src/.backup/game_core/game/achievements/seed.ts delete mode 100644 src/.backup/game_core/game/achievements/service.ts delete mode 100644 src/.backup/game_core/game/combat/attacksWorker.ts delete mode 100644 src/.backup/game_core/game/combat/equipmentService.ts delete mode 100644 src/.backup/game_core/game/combat/statusEffectsService.ts delete mode 100644 src/.backup/game_core/game/consumables/service.ts delete mode 100644 src/.backup/game_core/game/consumables/utils.ts delete mode 100644 src/.backup/game_core/game/cooldowns/service.ts delete mode 100644 src/.backup/game_core/game/core/userService.ts delete mode 100644 src/.backup/game_core/game/core/utils.ts delete mode 100644 src/.backup/game_core/game/economy/seedPurgePotion.ts delete mode 100644 src/.backup/game_core/game/economy/service.ts delete mode 100644 src/.backup/game_core/game/economy/types.ts delete mode 100644 src/.backup/game_core/game/lib/rpgFormat.ts delete mode 100644 src/.backup/game_core/game/lib/toolBreakLog.ts delete mode 100644 src/.backup/game_core/game/minigames/demoRun.ts delete mode 100644 src/.backup/game_core/game/minigames/seed.ts delete mode 100644 src/.backup/game_core/game/minigames/service.ts delete mode 100644 src/.backup/game_core/game/minigames/testHelpers.ts delete mode 100644 src/.backup/game_core/game/minigames/types.ts delete mode 100644 src/.backup/game_core/game/mobs/README.md delete mode 100644 src/.backup/game_core/game/mobs/admin.ts delete mode 100644 src/.backup/game_core/game/mobs/mobData.ts delete mode 100644 src/.backup/game_core/game/mutations/service.ts delete mode 100644 src/.backup/game_core/game/quests/service.ts delete mode 100644 src/.backup/game_core/game/rewards/service.ts delete mode 100644 src/.backup/game_core/game/smelting/service.ts delete mode 100644 src/.backup/game_core/game/stats/service.ts delete mode 100644 src/.backup/game_core/game/stats/types.ts delete mode 100644 src/.backup/game_core/game/streaks/service.ts delete mode 100644 src/.backup/itemCreate.ts.backup2 delete mode 100644 src/.backup/itemEdit.ts.backup2 delete mode 100644 src/.backup/listChannels.backup.ts.txt delete mode 100644 src/.backup/mobCreate.ts.backup2 delete mode 100644 src/.backup/mobEdit.ts.backup2 delete mode 100644 src/.backup/offerCreate.ts.backup2 delete mode 100644 src/.backup/offerEdit.ts.backup2 delete mode 100644 src/.backup/player.ts.backup delete mode 100644 src/.backup/prompts/discord-api-expert.prompt.md delete mode 100644 src/.backup/prompts/discord-helper.prompt.md delete mode 100644 src/.backup/prompts/resource-checker.prompt.md delete mode 100644 src/.backup/prompts/ts-validation.prompt.md delete mode 100644 src/.backup/views.backup/index.ejs delete mode 100644 src/.backup/views.backup/layouts/layout.ejs delete mode 100644 src/.backup/views.backup/pages/index.ejs delete mode 100644 src/.backup/views.backup/partials/navbar.ejs delete mode 100644 src/.backup/views.backup/partials/rightSidebar.ejs delete mode 100644 src/.backup/views.backup/partials/sections/admin.ejs delete mode 100644 src/.backup/views.backup/partials/sections/alianzas.ejs delete mode 100644 src/.backup/views.backup/partials/sections/cofres.ejs delete mode 100644 src/.backup/views.backup/partials/sections/comandos-basicos.ejs delete mode 100644 src/.backup/views.backup/partials/sections/configuracion.ejs delete mode 100644 src/.backup/views.backup/partials/sections/consumibles.ejs delete mode 100644 src/.backup/views.backup/partials/sections/crafteo.ejs delete mode 100644 src/.backup/views.backup/partials/sections/creacion-contenido.ejs delete mode 100644 src/.backup/views.backup/partials/sections/economia.ejs delete mode 100644 src/.backup/views.backup/partials/sections/ejemplos-avanzados.ejs delete mode 100644 src/.backup/views.backup/partials/sections/ejemplos-basicos.ejs delete mode 100644 src/.backup/views.backup/partials/sections/encantamientos.ejs delete mode 100644 src/.backup/views.backup/partials/sections/estadisticas.ejs delete mode 100644 src/.backup/views.backup/partials/sections/faq.ejs delete mode 100644 src/.backup/views.backup/partials/sections/fundicion.ejs delete mode 100644 src/.backup/views.backup/partials/sections/ia.ejs delete mode 100644 src/.backup/views.backup/partials/sections/inventario-equipo.ejs delete mode 100644 src/.backup/views.backup/partials/sections/logros.ejs delete mode 100644 src/.backup/views.backup/partials/sections/minijuegos.ejs delete mode 100644 src/.backup/views.backup/partials/sections/misiones.ejs delete mode 100644 src/.backup/views.backup/partials/sections/primeros-pasos.ejs delete mode 100644 src/.backup/views.backup/partials/sections/racha.ejs delete mode 100644 src/.backup/views.backup/partials/sections/recordatorios.ejs delete mode 100644 src/.backup/views.backup/partials/sections/sistema-juego.ejs delete mode 100644 src/.backup/views.backup/partials/sections/tienda.ejs delete mode 100644 src/.backup/views.backup/partials/sections/tips.ejs delete mode 100644 src/.backup/views.backup/partials/toc.ejs delete mode 100644 src/commands/messages/alliaces/createDisplayComponent.ts.backup delete mode 100644 src/test_help_logic.ts delete mode 100644 test/.validate_dashboard_items_syntax.js delete mode 100644 test/example.ts.txt delete mode 100644 test/examples/featureFlagsCommands.ts delete mode 100644 test/invalid_mobs_backup.json delete mode 100644 test/mob.test.ts delete mode 100644 test/scheduled_mob_attack_backup.json delete mode 100644 test/tmp_acorn.js delete mode 100644 test/tmp_check.js delete mode 100644 test/tmp_find_parse_error.js delete mode 100644 test/tmp_line_parse.js delete mode 100644 test/tmp_print_script.js delete mode 100644 test/tmp_token_balance.js delete mode 100644 test/tmp_token_trace.js delete mode 100644 test/tmp_tokens_inspect.js delete mode 100644 test/unit/questsAchievements.unit.ts delete mode 100644 test/unit/rewardMods.test.ts delete mode 100644 test/unit/rewardMods.unit.ts diff --git a/.gitignore b/.gitignore index 7ec5bfb..8117e9c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ qodana.yaml # Binarios amayo +/.backup/ \ No newline at end of file diff --git a/convert-modals-v14-v2.ps1 b/convert-modals-v14-v2.ps1 deleted file mode 100644 index 088d483..0000000 --- a/convert-modals-v14-v2.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -# Script v2 para convertir createDisplayComponent.ts a v14 -$file = "src/commands/messages/alliaces/createDisplayComponent.ts" -$content = Get-Content $file -Raw - -# 1. Reemplazar modal de thumbnail (líneas 441-456) - con regex más simple -$content = $content -replace '(?s)const modal = createModal\(\{\s+title: "📎 Editar Thumbnail",\s+customId: `edit_thumbnail_modal_\$\{idx\}`,\s+fields: \[.+?\],\s+\}\);', 'const modal = createImageUrlModal({ customId: `edit_thumbnail_modal_${idx}`, currentUrl: textComp.thumbnail || "", title: "📎 Editar Thumbnail", });' - -# 2. Reemplazar extractor de thumbnail_input a image_input (línea 471-472) -$content = $content -replace 'const rawInput = modalInteraction\.components\s+\.getTextInputValue\("thumbnail_input"\)', 'const thumbnailUrl = modalInteraction.fields.getTextInputValue("image_input")' -$content = $content -replace 'rawInput', 'thumbnailUrl' - -# 3. Reemplazar modal de title (líneas 765-779) -$content = $content -replace '(?s)const modal = createModal\(\{\s+title: "Editar Título del Bloque",\s+customId: "edit_title_modal",\s+fields: \[.+?\],\s+\}\);', 'const modal = createTitleModal({ customId: "edit_title_modal", currentTitle: blockState.title || "", });' - -# 4. Reemplazar extractor de title (línea 788-790) -$content = $content -replace 'const newTitle = modalInteraction\.components\s+\.getTextInputValue\("title_input"\)', 'const newTitle = modalInteraction.fields.getTextInputValue("title_input")' - -# 5. Reemplazar el modal de description que quedó - buscar por "as const" pattern -$content = $content -replace '(?s)const modal = \{[\s\S]+?customId: "edit_description_modal",[\s\S]+?\} as const;', 'const modal = createDescriptionModal({ customId: "edit_description_modal", currentDescription: blockState.description || "", });' - -# 6. El extractor description ya está correcto (.fields), verificar - -# Guardar -Set-Content -Path $file -Value $content -NoNewline -Write-Host "✅ Archivo convertido exitosamente v2" diff --git a/convert-modals-v14-v3.ps1 b/convert-modals-v14-v3.ps1 deleted file mode 100644 index 989a932..0000000 --- a/convert-modals-v14-v3.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -# Script v3 - FINAL para modals restantes -$file = "src/commands/messages/alliaces/createDisplayComponent.ts" -$content = Get-Content $file -Raw - -# 1. Modal de color (líneas ~850-877) - objeto con "as const" -$content = $content -replace '(?s)const modal = \{\s+title: "Editar Color",[\s\S]+?customId: "edit_color",[\s\S]+?\} as const;', 'const modal = createColorModal({ customId: "edit_color", currentColor: blockState.color ? `#${blockState.color.toString(16).padStart(6, "0")}` : "", });' - -# 2. Extractor color_input -$content = $content -replace 'const colorValue = modalInteraction\.components\s+\.getTextInputValue\("color_input"\)', 'const colorValue = modalInteraction.fields.getTextInputValue("color_input")' - -# 3. Modal de addContent (líneas ~950-969) - objeto con "as const" -$content = $content -replace '(?s)const modal = \{\s+title: "Añadir Contenido",[\s\S]+?customId: "add_content",[\s\S]+?\} as const;', 'const modal = createTextContentModal({ customId: "add_content", });' - -# 4. Extractor content_input -$content = $content -replace 'const content = modalInteraction\.components\s+\.getTextInputValue\("content_input"\)', 'const content = modalInteraction.fields.getTextInputValue("content_input")' - -# 5. Modal de addImage (líneas 1073-1086) -$content = $content -replace '(?s)const modal = createModal\(\{\s+title: "Añadir Imagen",\s+customId: "add_image_modal",\s+fields: \[[\s\S]+?\],\s+\}\);', 'const modal = createImageUrlModal({ customId: "add_image_modal", title: "🖼️ Añadir Imagen", });' - -# 6. Extractor image_input para addImage -$content = $content -replace 'const imageUrl = modalInteraction\.components\s+\.getTextInputValue\("image_input"\)', 'const imageUrl = modalInteraction.fields.getTextInputValue("image_input")' - -# 7. Modal de coverImage (líneas ~1145-1164) - objeto con "as const" -$content = $content -replace '(?s)const modal = \{\s+title: "Imagen de Portada",[\s\S]+?customId: "cover_image",[\s\S]+?\} as const;', 'const modal = createImageUrlModal({ customId: "cover_image", title: "🖼️ Imagen de Portada", currentUrl: blockState.coverImage || "", });' - -# 8. Extractor cover_input -> image_input -$content = $content -replace 'const coverUrl = modalInteraction\.components\s+\.getTextInputValue\("cover_input"\)', 'const coverUrl = modalInteraction.fields.getTextInputValue("image_input")' - -# Guardar -Set-Content -Path $file -Value $content -NoNewline -Write-Host "✅ Modals restantes convertidos v3" diff --git a/convert-modals-v14-v4.ps1 b/convert-modals-v14-v4.ps1 deleted file mode 100644 index 4286c6d..0000000 --- a/convert-modals-v14-v4.ps1 +++ /dev/null @@ -1,106 +0,0 @@ -# Script v4 - Reemplazar los 3 modals exactos que quedaron -$file = "src/commands/messages/alliaces/createDisplayComponent.ts" -$content = Get-Content $file -Raw - -# 1. handleEditColor (líneas 857-877) - EXACTO -$oldColorModal = @' - const modal = { - title: "Editar Color del Bloque", - customId: "edit_color_modal", - components: [ - { - type: ComponentType.Label, - label: "Color (formato HEX)", - component: { - type: ComponentType.TextInput, - customId: "color_input", - style: TextInputStyle.Short, - required: false, - placeholder: "#FF5733 o FF5733", - value: blockState.color - ? `#${blockState.color.toString(16).padStart(6, "0")}` - : "", - maxLength: 7, - }, - }, - ], - } as const; -'@ - -$newColorModal = @' - const modal = createColorModal({ - customId: "edit_color_modal", - currentColor: blockState.color - ? `#${blockState.color.toString(16).padStart(6, "0")}` - : "", - }); -'@ - -$content = $content.Replace($oldColorModal, $newColorModal) - -# 2. handleAddContent (líneas 951-968) - EXACTO -$oldContentModal = @' - const modal = { - title: "Añadir Contenido de Texto", - customId: "add_content_modal", - components: [ - { - type: ComponentType.Label, - label: "Contenido", - component: { - type: ComponentType.TextInput, - customId: "content_input", - style: TextInputStyle.Paragraph, - required: true, - placeholder: "Escribe el contenido de texto...", - maxLength: 4000, - }, - }, - ], - } as const; -'@ - -$newContentModal = @' - const modal = createTextContentModal({ - customId: "add_content_modal", - }); -'@ - -$content = $content.Replace($oldContentModal, $newContentModal) - -# 3. handleCoverImage (líneas 1130-1148) - EXACTO -$oldCoverModal = @' - const modal = { - title: "Imagen de Portada", - customId: "cover_image_modal", - components: [ - { - type: ComponentType.Label, - label: "URL de la Imagen de Portada", - component: { - type: ComponentType.TextInput, - customId: "cover_input", - style: TextInputStyle.Short, - required: false, - placeholder: "https://ejemplo.com/portada.png", - value: blockState.coverImage || "", - maxLength: 512, - }, - }, - ], - } as const; -'@ - -$newCoverModal = @' - const modal = createImageUrlModal({ - customId: "cover_image_modal", - title: "🖼️ Imagen de Portada", - currentUrl: blockState.coverImage || "", - }); -'@ - -$content = $content.Replace($oldCoverModal, $newCoverModal) - -# Guardar -Set-Content -Path $file -Value $content -NoNewline -Write-Host "✅ Últimos 3 modals convertidos v4" diff --git a/convert-modals-v14.ps1 b/convert-modals-v14.ps1 deleted file mode 100644 index 3e86134..0000000 --- a/convert-modals-v14.ps1 +++ /dev/null @@ -1,58 +0,0 @@ -# Script para convertir createDisplayComponent.ts a v14 -$file = "src/commands/messages/alliaces/createDisplayComponent.ts" -$content = Get-Content $file -Raw - -# 1. Agregar imports -$importSection = @" -import type { DisplayComponentContainer } from "../../../core/types/displayComponents"; -import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; -// v14-compatible modal builders -import { - createTitleModal, - createDescriptionModal, - createColorModal, - createTextContentModal, - createImageUrlModal, -} from "../../../core/lib/displayComponents"; -"@ - -$content = $content -replace 'import type \{ DisplayComponentContainer \} from "\.\.\/\.\.\/\.\.\/core\/types\/displayComponents";\r?\nimport \{ hasManageGuildOrStaff \} from "\.\.\/\.\.\/\.\.\/core\/lib\/permissions";', $importSection - -# 2. Comentar función createModal vieja -$oldCreateModal = 'type ModalField[\s\S]*?} as const;\r?\n}' -$newCreateModal = @" -// OLD v15-dev createModal() - DISABLED -/* type ModalField = { - customId: string; style: number; placeholder?: string; value?: string; - required?: boolean; maxLength?: number; label?: string; -}; -function createModal(params: { title: string; customId: string; fields: ModalField[]; }) { - const components = params.fields.map((f) => ({ type: ComponentType.Label, label: f.label ?? "", component: { type: ComponentType.TextInput, customId: f.customId, style: f.style, placeholder: f.placeholder, value: f.value, required: f.required ?? false, maxLength: f.maxLength, }, })); - return { title: params.title, customId: params.customId, components, } as const; -} */ -"@ - -$content = $content -replace $oldCreateModal, $newCreateModal - -# 3. Reemplazar modal de thumbnail (línea ~469) -$content = $content -replace 'const modal = createModal\(\{\s+title: "📎 Editar Thumbnail",\s+customId: `edit_thumbnail_modal_\$\{idx\}`,\s+fields: \[\s+\{\s+customId: "thumbnail_input",[\s\S]*?\}\s+\],\s+\}\);', 'const modal = createImageUrlModal({ customId: `edit_thumbnail_modal_${idx}`, currentUrl: textComp.thumbnail || "", title: "📎 Editar Thumbnail", });' - -# 4. Reemplazar extractors thumbnail_input -> image_input -$content = $content -replace '\.components\.getTextInputValue\("thumbnail_input"\)', '.fields.getTextInputValue("image_input")' - -# 5. Reemplazar modal de title (línea ~793) -$content = $content -replace 'const modal = createModal\(\{\s+title: "Editar Título del Bloque",\s+customId: "edit_title_modal",\s+fields: \[\s+\{\s+customId: "title_input",[\s\S]*?\}\s+\],\s+\}\);', 'const modal = createTitleModal({ customId: "edit_title_modal", currentTitle: blockState.title || "", });' - -# 6. Reemplazar modal de image (línea ~1150) -$content = $content -replace 'const modal = createModal\(\{\s+title: "Añadir Imagen",\s+customId: "add_image_modal",\s+fields: \[\s+\{\s+customId: "image_input",[\s\S]*?\}\s+\],\s+\}\);', 'const modal = createImageUrlModal({ customId: "add_image_modal", title: "🖼️ Añadir Imagen", });' - -# 7. Reemplazar .components.getTextInputValue por .fields.getTextInputValue -$content = $content -replace '\.components\.getTextInputValue\(', '.fields.getTextInputValue(' - -# 8. Remover (as any) de showModal -$content = $content -replace 'await interaction\.showModal\(modal as any\);', 'await interaction.showModal(modal);' -$content = $content -replace 'await sel\.showModal\(modal as any\);', 'await sel.showModal(modal);' - -# Guardar -Set-Content -Path $file -Value $content -NoNewline -Write-Host "✅ Archivo convertido exitosamente" diff --git a/scripts/check_brackets.js b/scripts/check_brackets.js deleted file mode 100644 index f1c2b9e..0000000 --- a/scripts/check_brackets.js +++ /dev/null @@ -1,51 +0,0 @@ -const fs = require('fs'); -const path = 'src/server/public/assets/js/dashboard_items.js'; -const src = fs.readFileSync(path, 'utf8'); - -let stack = []; -let inSingle = false, inDouble = false, inTpl = false; -let inLineComment = false, inBlockComment = false; - -for (let i = 0; i < src.length; i++) { - const ch = src[i]; - const prev = src[i - 1]; - const next = src[i + 1]; - - // comments - if (!inSingle && !inDouble && !inTpl) { - if (!inBlockComment && ch === '/' && next === '/') { inLineComment = true; continue; } - if (!inLineComment && ch === '/' && next === '*') { inBlockComment = true; i++; continue; } - } - if (inLineComment) { if (ch === '\n') inLineComment = false; continue; } - if (inBlockComment) { if (ch === '*' && next === '/') { inBlockComment = false; i++; } continue; } - - // strings - if (!inDouble && !inTpl && ch === "'" && prev !== '\\') { inSingle = !inSingle; continue; } - if (!inSingle && !inTpl && ch === '"' && prev !== '\\') { inDouble = !inDouble; continue; } - if (!inSingle && !inDouble && ch === '`' && prev !== '\\') { inTpl = !inTpl; continue; } - if (inSingle || inDouble || inTpl) continue; - - if (ch === '(' || ch === '{' || ch === '[') stack.push({ ch, pos: i }); - if (ch === ')' || ch === '}' || ch === ']') { - const last = stack.pop(); - if (!last) { console.log('UNMATCHED_CLOSE', ch, 'at index', i); process.exit(0); } - const map = { '(': ')', '{': '}', '[': ']' }; - if (map[last.ch] !== ch) { console.log('MISMATCH', last.ch, 'opened at', last.pos, 'but closed by', ch, 'at', i); process.exit(0); } - } -} - -if (stack.length) { - const last = stack[stack.length - 1]; - const upTo = src.slice(0, last.pos); - const line = upTo.split('\n').length; - const col = last.pos - upTo.lastIndexOf('\n'); - console.log('UNMATCHED_OPEN', last.ch, 'at index', last.pos, 'line', line, 'col', col); - console.log('--- context ---'); - const start = Math.max(0, last.pos - 120); - const end = Math.min(src.length, last.pos + 120); - console.log(src.slice(start, end)); - process.exit(2); -} - -console.log('BALANCED'); -process.exit(0); diff --git a/scripts/check_nav_render.js b/scripts/check_nav_render.js deleted file mode 100644 index 1a9774c..0000000 --- a/scripts/check_nav_render.js +++ /dev/null @@ -1,73 +0,0 @@ -const ejs = require('../node_modules/ejs'); -const path = require('path'); -(async () => { - try { - const viewsRoot = path.join(process.cwd(), 'src', 'server', 'views'); - const layoutPath = path.join(viewsRoot, 'layouts', 'layout.ejs'); - - // Dashboard (should NOT include navbar) - const dashboardPage = await ejs.renderFile( - path.join(viewsRoot, 'pages', 'dashboard.ejs'), - { - user: { id: '123', username: 'tester', avatar: '' }, - guilds: [{ id: '1', name: 'G1' }], - botAvailable: true, - selectedGuildId: '1', - }, - { async: true, views: [viewsRoot] } - ); - const dashboardHtml = await ejs.renderFile( - layoutPath, - { - appName: 'Amayo', - version: '2.0.0', - body: dashboardPage, - hideNavbar: true, - useDashboardNav: false, - dashboardNav: null, - navbar: null, - session: { user: { id: '123', username: 'tester', avatar: '' } }, - selectedGuildName: 'G1', - selectedGuildId: '1', - }, - { async: true, views: [viewsRoot] } - ); - - // Labs (should INCLUDE dashboard nav) - const labsPage = await ejs.renderFile( - path.join(viewsRoot, 'pages', 'labs.ejs'), - { selectedGuildName: 'G1', selectedGuildId: '1' }, - { async: true, views: [viewsRoot] } - ); - const labsHtml = await ejs.renderFile( - layoutPath, - { - appName: 'Amayo', - version: '2.0.0', - body: labsPage, - hideNavbar: false, - useDashboardNav: true, - session: { user: { id: '123', username: 'tester', avatar: '' } }, - selectedGuildName: 'G1', - selectedGuildId: '1', - }, - { async: true, views: [viewsRoot] } - ); - - const dashHasNav = Boolean(dashboardHtml.match(/]*>/i)); - const labsHasNav = Boolean(labsHtml.match(/]*>/i)); - const labsHasGuild = labsHtml.includes('G1'); - - console.log('DASHBOARD_HAS_NAV:' + dashHasNav); - console.log('LABS_HAS_NAV:' + labsHasNav); - console.log('LABS_INCLUDES_GUILD:' + labsHasGuild); - console.log('\n--- DASHBOARD SNIPPET ---\n'); - console.log(dashboardHtml.slice(0, 800)); - console.log('\n--- LABS SNIPPET ---\n'); - console.log(labsHtml.slice(0, 800)); - } catch (err) { - console.error('RENDER_CHECK_ERROR', err); - process.exit(1); - } -})(); - diff --git a/scripts/cleanInvalidMobs.ts b/scripts/cleanInvalidMobs.ts deleted file mode 100644 index c52a59b..0000000 --- a/scripts/cleanInvalidMobs.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { prisma } from "../src/core/database/prisma"; -import { BaseMobDefinitionSchema } from "../src/game/mobs/mobData"; - -async function run() { - if (!process.env.XATA_DB) { - console.error("XATA_DB not set — aborting"); - process.exit(1); - } - console.log("Scanning mobs table for invalid definitions..."); - const rows: any[] = await (prisma as any).mob.findMany(); - const invalid: any[] = []; - for (const r of rows) { - const cfg = - r.metadata ?? - r.stats ?? - r.drops ?? - r.config ?? - r.definition ?? - r.data ?? - null; - try { - BaseMobDefinitionSchema.parse(cfg as any); - } catch (e) { - invalid.push({ id: r.id, error: (e as any)?.errors ?? e, row: r }); - } - } - if (invalid.length === 0) { - console.log("No invalid mob definitions found."); - process.exit(0); - } - console.log( - `Found ${invalid.length} invalid rows. Backing up and deleting...` - ); - // backup - console.log("Backup file: invalid_mobs_backup.json"); - require("fs").writeFileSync( - "invalid_mobs_backup.json", - JSON.stringify(invalid, null, 2) - ); - for (const it of invalid) { - try { - await (prisma as any).mob.delete({ where: { id: it.id } }); - console.log("Deleted invalid mob id=", it.id); - } catch (e) { - console.warn("Failed to delete id=", it.id, e); - } - } - console.log("Cleanup complete. Review invalid_mobs_backup.json"); -} - -run().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/scripts/collab-tests/dashboard/README.md b/scripts/collab-tests/dashboard/README.md deleted file mode 100644 index 631481e..0000000 --- a/scripts/collab-tests/dashboard/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Pruebas colaborativas - Dashboard - -Objetivo - -- Producir una prueba rápida que renderice la plantilla `dashboard` y detecte si aparece la cadena "[object Promise]" en el HTML final. - -Ejecutar (solo colaboradores) - -- Para evitar ejecuciones accidentales, este script solo se ejecuta si la variable de entorno `COLLAB_TEST` está establecida en `1`. - -Ejemplo: - -```bash -COLLAB_TEST=1 ./scripts/collab-tests/dashboard/run.sh -``` - -Salida - -- El script imprimirá un JSON con la forma: - - - ok: true|false - - length: longitud del HTML renderizado - - foundIndex: índice de "[object Promise]" o -1 - -Notas - -- Este test está pensado para replicar exactamente el pipeline de EJS con `{ async: true }` y los locals mínimos necesarios. Si el test falla en tu entorno de desarrollo, revisa las modificaciones temporales en locales u otras plantillas. - -Uso de la fuente BoldPixels.ttf - -- Copia de la fuente del repo: `src/server/public/assets/fonts/BoldPixels.ttf`. - -- Añade la regla CSS siguiente (por ejemplo en `src/server/public/assets/css/_fonts.css`) y luego importa ese archivo en tu layout o en `styles.css`: - -```css -@font-face { - font-family: 'BoldPixels'; - src: url('/assets/fonts/BoldPixels.ttf') format('truetype'); - font-weight: 700; - font-style: normal; -} - -.bold-pixels { - font-family: 'BoldPixels', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; -} -``` - -Ejemplo de uso en EJS: - -```html -

Título con fuente BoldPixels

-``` diff --git a/scripts/collab-tests/dashboard/check_dashboard_render.js b/scripts/collab-tests/dashboard/check_dashboard_render.js deleted file mode 100644 index d7fe6b7..0000000 --- a/scripts/collab-tests/dashboard/check_dashboard_render.js +++ /dev/null @@ -1,35 +0,0 @@ -const ejs = require('ejs'); -const path = require('path'); -const fs = require('fs'); - -(async function(){ - try{ - const cwd = process.cwd(); - const viewsDir = path.join(cwd,'src','server','views'); - const pageFile = path.join(viewsDir,'pages','dashboard.ejs'); - const layoutFile = path.join(viewsDir,'layouts','layout.ejs'); - - const locals = { - appName:'amayo', - user:{username:'collab-test',id:'1',avatar:''}, - guilds:[], - useDashboardNav:false, - version:'test', - selectedGuild:null, - title:'Dashboard collab test', - hideNavbar:false - }; - - const pageBody = await ejs.renderFile(pageFile, locals, {async:true, views:[viewsDir]}); - const html = await ejs.renderFile(layoutFile, {...locals, body: pageBody, dashboardNav:null, navbar:null}, {async:true, views:[viewsDir]}); - - const found = html.indexOf('[object Promise]') !== -1; - const out = { ok: !found, length: html.length, foundIndex: found ? html.indexOf('[object Promise]') : -1 }; - - console.log(JSON.stringify(out, null, 2)); - if(found) process.exitCode = 2; - }catch(err){ - console.error('ERROR', err && err.stack ? err.stack : err); - process.exitCode = 3; - } -})(); diff --git a/scripts/collab-tests/dashboard/check_roles_render.js b/scripts/collab-tests/dashboard/check_roles_render.js deleted file mode 100644 index 67743b0..0000000 --- a/scripts/collab-tests/dashboard/check_roles_render.js +++ /dev/null @@ -1,42 +0,0 @@ -const ejs = require('ejs'); -const path = require('path'); - -(async ()=>{ - try{ - const cwd = process.cwd(); - const viewsDir = path.join(cwd,'src','server','views'); - const pageFile = path.join(viewsDir,'pages','dashboard.ejs'); - const layoutFile = path.join(viewsDir,'layouts','layout.ejs'); - - const mockRoles = [ - { id: '111', name: 'Admin', color: '#ff0000' }, - { id: '222', name: 'Mods', color: '#00ff00' }, - ]; - - const locals = { - appName: 'amayo', - user: { username: 'test', id: '1' }, - guilds: [], - selectedGuild: 'guild1', - selectedGuildId: 'guild1', - selectedGuildName: 'Guild One', - guildConfig: { staff: ['111'] }, - guildRoles: mockRoles, - useDashboardNav: true, - hideNavbar: false, - page: 'settings' - }; - - const pageBody = await ejs.renderFile(pageFile, locals, { async: true, views:[viewsDir] }); - const html = await ejs.renderFile(layoutFile, {...locals, body: pageBody, dashboardNav: null, navbar: null, title: 'Dashboard test', version: 'test'}, { async: true, views:[viewsDir] }); - - const hasAdmin = html.indexOf('Admin') !== -1; - const hasMods = html.indexOf('Mods') !== -1; - const hasSwatch = html.indexOf('background:#ff0000') !== -1 || html.indexOf('background:#00ff00') !== -1; - console.log({ hasAdmin, hasMods, hasSwatch, length: html.length }); - process.exit(hasAdmin && hasMods && hasSwatch ? 0 : 2); - }catch(err){ - console.error(err && err.stack ? err.stack : err); - process.exit(3); - } -})(); diff --git a/scripts/collab-tests/dashboard/run.sh b/scripts/collab-tests/dashboard/run.sh deleted file mode 100755 index de9e808..0000000 --- a/scripts/collab-tests/dashboard/run.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -# Small wrapper to run the collab test only when COLLAB_TEST=1 is set. -set -euo pipefail - -if [ "${COLLAB_TEST:-0}" != "1" ]; then - echo "This test is for collaborators only. Set COLLAB_TEST=1 to run it." - echo "Example: COLLAB_TEST=1 ./scripts/collab-tests/dashboard/run.sh" - exit 1 -fi - -node ./scripts/collab-tests/dashboard/check_dashboard_render.js diff --git a/scripts/debugFeatureFlags.ts b/scripts/debugFeatureFlags.ts deleted file mode 100644 index 3dee38f..0000000 --- a/scripts/debugFeatureFlags.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Debug del sistema de Feature Flags - */ - -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); - -async function debugFeatureFlags() { - console.log("🔍 Debugging Feature Flags...\n"); - - try { - // Listar todos los flags - const flags = await prisma.featureFlag.findMany(); - console.log(`📊 Total de flags en DB: ${flags.length}\n`); - - if (flags.length === 0) { - console.log("ℹ️ No hay flags en la base de datos"); - return; - } - - // Mostrar cada flag - for (const flag of flags) { - console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); - console.log(`🎯 Flag: ${flag.name}`); - console.log(` ID: ${flag.id}`); - console.log(` Status: ${flag.status}`); - console.log(` Target: ${flag.target}`); - console.log(` Description: ${flag.description || "N/A"}`); - console.log(` Rollout Strategy: ${flag.rolloutStrategy || "N/A"}`); - console.log(` Rollout Config: ${flag.rolloutConfig || "N/A"}`); - console.log(` Start Date: ${flag.startDate || "N/A"}`); - console.log(` End Date: ${flag.endDate || "N/A"}`); - console.log(` Created: ${flag.createdAt}`); - console.log(` Updated: ${flag.updatedAt}`); - - // Verificar si hay problemas con los datos - if (flag.rolloutConfig) { - try { - const parsed = JSON.parse(flag.rolloutConfig); - console.log(` ✅ Rollout Config parseable:`, parsed); - } catch (e: any) { - console.log(` ❌ ERROR parseando Rollout Config: ${e.message}`); - } - } - - if (flag.metadata) { - try { - const parsed = JSON.parse(flag.metadata); - console.log(` ✅ Metadata parseable:`, parsed); - } catch (e: any) { - console.log(` ❌ ERROR parseando Metadata: ${e.message}`); - } - } - } - - console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); - - // Test de creación - console.log("🧪 Test: Crear flag temporal..."); - const testFlag = await prisma.featureFlag.create({ - data: { - name: `test_flag_${Date.now()}`, - description: "Flag de test temporal", - status: "disabled", - target: "global", - }, - }); - console.log(`✅ Flag creado: ${testFlag.name}`); - - // Limpiar - await prisma.featureFlag.delete({ - where: { name: testFlag.name }, - }); - console.log(`🗑️ Flag eliminado: ${testFlag.name}\n`); - - console.log("✅ Sistema funcionando correctamente"); - } catch (error: any) { - console.error("❌ ERROR:", error.message); - console.error("Stack:", error.stack); - console.error("Code:", error.code); - console.error("Meta:", error.meta); - } finally { - await prisma.$disconnect(); - } -} - -debugFeatureFlags(); diff --git a/scripts/debugInventory.ts b/scripts/debugInventory.ts deleted file mode 100644 index a0f2c9d..0000000 --- a/scripts/debugInventory.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Script de Debug: Inspeccionar inventario de usuario específico - * - * Verifica estado actual de items de herramientas para diagnosticar el problema - */ - -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); - -async function main() { - const userId = process.argv[2]; - const guildId = process.argv[3]; - - if (!userId || !guildId) { - console.error( - "❌ Uso: npx tsx scripts/debugInventory.ts " - ); - process.exit(1); - } - - console.log( - `🔍 Inspeccionando inventario de usuario ${userId.slice( - 0, - 8 - )}... en guild ${guildId.slice(0, 8)}...\n` - ); - - // Obtener todas las entradas de inventario del usuario - const entries = await prisma.inventoryEntry.findMany({ - where: { userId, guildId }, - include: { item: true }, - }); - - console.log(`📦 Total de items: ${entries.length}\n`); - - for (const entry of entries) { - const item = entry.item; - const state = entry.state as any; - const instances = state?.instances ?? []; - - console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); - console.log(`📦 Item: ${item.name} (${item.key})`); - console.log(` Stackable: ${item.stackable}`); - console.log(` Quantity: ${entry.quantity}`); - console.log(` Props:`, JSON.stringify(item.props, null, 2)); - console.log(` State.instances:`, JSON.stringify(instances, null, 2)); - - if (!item.stackable && entry.quantity > 1 && instances.length === 0) { - console.log( - ` ⚠️ PROBLEMA: Non-stackable con quantity>1 pero sin instances` - ); - } - - if (instances.length > 0) { - console.log(` 📊 Resumen de instancias:`); - instances.forEach((inst: any, idx: number) => { - console.log(` [${idx}] Durabilidad: ${inst.durability ?? "N/A"}`); - }); - } - console.log(""); - } - - // Verificar equipo - const equipment = await prisma.playerEquipment.findUnique({ - where: { userId_guildId: { userId, guildId } }, - }); - - if (equipment) { - console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); - console.log(`🧰 Equipo equipado:`); - if (equipment.weaponItemId) { - const weapon = await prisma.economyItem.findUnique({ - where: { id: equipment.weaponItemId }, - }); - console.log(` Arma: ${weapon?.name ?? "Desconocida"} (${weapon?.key})`); - } else { - console.log(` Arma: ❌ NINGUNA EQUIPADA`); - } - - if (equipment.armorItemId) { - const armor = await prisma.economyItem.findUnique({ - where: { id: equipment.armorItemId }, - }); - console.log( - ` Armadura: ${armor?.name ?? "Desconocida"} (${armor?.key})` - ); - } else { - console.log(` Armadura: (Ninguna)`); - } - - if (equipment.capeItemId) { - const cape = await prisma.economyItem.findUnique({ - where: { id: equipment.capeItemId }, - }); - console.log(` Capa: ${cape?.name ?? "Desconocida"} (${cape?.key})`); - } else { - console.log(` Capa: (Ninguna)`); - } - } else { - console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); - console.log(`🧰 Equipo: ❌ Sin registro de equipo`); - } -} - -main() - .catch((error) => { - console.error("❌ Error:", error); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); diff --git a/scripts/findMobDependencies.ts b/scripts/findMobDependencies.ts deleted file mode 100644 index b6a52ac..0000000 --- a/scripts/findMobDependencies.ts +++ /dev/null @@ -1,80 +0,0 @@ -import fs from "fs"; -import { prisma } from "../src/core/database/prisma"; - -async function run() { - if (!process.env.XATA_DB) { - console.error("XATA_DB not set — aborting"); - process.exit(1); - } - - if (!fs.existsSync("invalid_mobs_backup.json")) { - console.error("invalid_mobs_backup.json not found — run cleanup first"); - process.exit(1); - } - - const bak = JSON.parse(fs.readFileSync("invalid_mobs_backup.json", "utf8")); - const ids: string[] = bak.map((b: any) => b.id).filter(Boolean); - if (ids.length === 0) { - console.log("No ids found in invalid_mobs_backup.json"); - return; - } - - console.log("Looking for FK constraints that reference the Mob table..."); - const fkSql = ` - SELECT - tc.constraint_name, kcu.table_name, kcu.column_name, ccu.table_name AS referenced_table, ccu.column_name AS referenced_column - FROM information_schema.table_constraints AS tc - JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name AND tc.constraint_schema = kcu.constraint_schema - JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name AND ccu.constraint_schema = tc.constraint_schema - WHERE tc.constraint_type = 'FOREIGN KEY' AND LOWER(ccu.table_name) = 'mob' - `; - - const refs: any[] = await (prisma as any).$queryRawUnsafe(fkSql); - if (!refs || refs.length === 0) { - console.log("No FK constraints found referencing mob table."); - return; - } - - console.log("Found referencing constraints:"); - for (const r of refs) { - console.log( - ` - ${r.table_name}.${r.column_name} -> ${r.referenced_table}.${r.referenced_column} (constraint ${r.constraint_name})` - ); - } - - // For each referencing table/column, search rows that use our ids - for (const r of refs) { - const table = r.table_name; - const column = r.column_name; - console.log( - `\nChecking table ${table} (column ${column}) for dependent rows...` - ); - for (const id of ids) { - try { - const cntRes: any[] = await (prisma as any).$queryRawUnsafe( - `SELECT COUNT(*) AS cnt FROM "${table}" WHERE "${column}" = '${id}'` - ); - const cnt = - cntRes && cntRes[0] - ? Number(cntRes[0].cnt || cntRes[0].count || 0) - : 0; - console.log(` mob id=${id} -> ${cnt} dependent row(s)`); - if (cnt > 0) { - const rows: any[] = await (prisma as any).$queryRawUnsafe( - `SELECT * FROM "${table}" WHERE "${column}" = '${id}' LIMIT 5` - ); - console.log(" Sample rows:", rows); - } - } catch (e) { - console.warn(" Failed to query", table, column, e?.message ?? e); - } - } - } - - console.log("\nDependency scan complete."); -} - -run().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/scripts/fullServerSetup.ts b/scripts/fullServerSetup.ts deleted file mode 100644 index 1e1c19a..0000000 --- a/scripts/fullServerSetup.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { prisma } from "../src/core/database/prisma"; -import { Prisma } from "@prisma/client"; - -/** - * fullServerSetup.ts - * - * Script idempotente para poblar UN servidor con todo lo necesario: - * - Economy items (herramientas, armas, materiales, cofres, pociones) - * - Item recipes (crafteo) - * - Item mutations (encantamientos) - * - Game areas y niveles - * - Mobs con drops - * - Opcional: programar ataques de mobs demo - * - * Uso: provee GUILD_ID como variable de entorno opcional. Si no se provee, usa el id por defecto. - * GUILD_ID=1316592320954630144 npx tsx scripts/fullServerSetup.ts - */ - -const DEFAULT_GUILD = process.env.GUILD_ID ?? "1316592320954630144"; - -async function upsertEconomyItem( - guildId: string | null, - key: string, - data: Omit -) { - const existing = await prisma.economyItem.findFirst({ - where: { key, guildId }, - }); - if (existing) - return prisma.economyItem.update({ - where: { id: existing.id }, - data: { ...data }, - }); - return prisma.economyItem.create({ data: { ...data, key, guildId } }); -} - -async function upsertGameArea( - guildId: string | null, - key: string, - data: Omit -) { - const existing = await prisma.gameArea.findFirst({ where: { key, guildId } }); - if (existing) - return prisma.gameArea.update({ - where: { id: existing.id }, - data: { ...data }, - }); - return prisma.gameArea.create({ data: { ...data, key, guildId } }); -} - -async function upsertMob( - guildId: string | null, - key: string, - data: Omit -) { - const existing = await prisma.mob.findFirst({ where: { key, guildId } }); - if (existing) - return prisma.mob.update({ where: { id: existing.id }, data: { ...data } }); - return prisma.mob.create({ data: { ...data, key, guildId } }); -} - -async function upsertItemRecipe( - guildId: string | null, - productKey: string, - ingredients: { itemKey: string; qty: number }[], - productQty = 1 -) { - // Ensure product exists - const product = await prisma.economyItem.findFirst({ - where: { key: productKey, OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); - if (!product) throw new Error(`Product item not found: ${productKey}`); - - // Find existing recipe by productItemId - const existing = await prisma.itemRecipe.findUnique({ - where: { productItemId: product.id }, - }); - if (existing) { - // Recreate ingredients set - await prisma.recipeIngredient.deleteMany({ - where: { recipeId: existing.id }, - }); - for (const ing of ingredients) { - const it = await prisma.economyItem.findFirst({ - where: { key: ing.itemKey, OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); - if (!it) throw new Error(`Ingredient item not found: ${ing.itemKey}`); - await prisma.recipeIngredient.create({ - data: { recipeId: existing.id, itemId: it.id, quantity: ing.qty }, - }); - } - return existing; - } - - const r = await prisma.itemRecipe.create({ - data: { productItemId: product.id, productQuantity: productQty }, - }); - for (const ing of ingredients) { - const it = await prisma.economyItem.findFirst({ - where: { key: ing.itemKey, OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); - if (!it) throw new Error(`Ingredient item not found: ${ing.itemKey}`); - await prisma.recipeIngredient.create({ - data: { recipeId: r.id, itemId: it.id, quantity: ing.qty }, - }); - } - return r; -} - -export async function runFullServerSetup( - guildIdArg?: string | null, - options?: { dryRun?: boolean } -) { - const guildId = guildIdArg ?? DEFAULT_GUILD; - console.log("Starting full server setup for guild=", guildId, options ?? {}); - - // --- Items: tools, weapons, materials --- - await upsertEconomyItem(guildId, "tool.pickaxe.basic", { - name: "Pico Básico", - stackable: false, - props: { - tool: { type: "pickaxe", tier: 1 }, - breakable: { enabled: true, maxDurability: 100, durabilityPerUse: 5 }, - } as unknown as Prisma.InputJsonValue, - tags: ["tool", "mine"], - }); - - await upsertEconomyItem(guildId, "tool.pickaxe.iron", { - name: "Pico de Hierro", - stackable: false, - props: { - tool: { type: "pickaxe", tier: 2 }, - breakable: { enabled: true, maxDurability: 180, durabilityPerUse: 4 }, - } as unknown as Prisma.InputJsonValue, - tags: ["tool", "mine", "tier2"], - }); - - await upsertEconomyItem(guildId, "weapon.sword.iron", { - name: "Espada de Hierro", - stackable: false, - props: { - damage: 10, - tool: { type: "sword", tier: 1 }, - breakable: { enabled: true, maxDurability: 150, durabilityPerUse: 2 }, - } as unknown as Prisma.InputJsonValue, - tags: ["weapon"], - }); - - await upsertEconomyItem(guildId, "armor.leather.basic", { - name: "Armadura de Cuero", - stackable: false, - props: { defense: 3 } as unknown as Prisma.InputJsonValue, - tags: ["armor"], - }); - - await upsertEconomyItem(guildId, "ore.iron", { - name: "Mineral de Hierro", - stackable: true, - props: { craftingOnly: true } as unknown as Prisma.InputJsonValue, - tags: ["ore", "common"], - }); - await upsertEconomyItem(guildId, "ingot.iron", { - name: "Lingote de Hierro", - stackable: true, - props: {} as unknown as Prisma.InputJsonValue, - tags: ["ingot", "metal"], - }); - - // Consumibles y pociones - await upsertEconomyItem(guildId, "food.meat.small", { - name: "Carne Pequeña", - stackable: true, - props: { - food: { healHp: 8, cooldownSeconds: 20 }, - } as unknown as Prisma.InputJsonValue, - tags: ["food"], - }); - await upsertEconomyItem(guildId, "potion.energy", { - name: "Poción Energética", - stackable: true, - props: { - potion: { removeEffects: ["FATIGUE"], cooldownSeconds: 90 }, - } as unknown as Prisma.InputJsonValue, - tags: ["potion", "utility"], - }); - - // Cofre con recompensas - await upsertEconomyItem(guildId, "chest.daily", { - name: "Cofre Diario", - stackable: true, - props: { - chest: { - enabled: true, - consumeOnOpen: true, - randomMode: "single", - rewards: [ - { type: "coins", amount: 200 }, - { type: "item", itemKey: "ingot.iron", qty: 2 }, - ], - }, - } as unknown as Prisma.InputJsonValue, - tags: ["chest"], - }); - - // --- Mutations / enchants catalog --- - // Item mutations (catalog) - const existingRuby = await prisma.itemMutation.findFirst({ - where: { key: "ruby_core", guildId }, - }); - if (existingRuby) { - await prisma.itemMutation.update({ - where: { id: existingRuby.id }, - data: { name: "Núcleo de Rubí", effects: { damageBonus: 15 } as any }, - }); - } else { - await prisma.itemMutation.create({ - data: { - key: "ruby_core", - name: "Núcleo de Rubí", - guildId, - effects: { damageBonus: 15 } as any, - } as any, - }); - } - - const existingEmerald = await prisma.itemMutation.findFirst({ - where: { key: "emerald_core", guildId }, - }); - if (existingEmerald) { - await prisma.itemMutation.update({ - where: { id: existingEmerald.id }, - data: { - name: "Núcleo de Esmeralda", - effects: { defenseBonus: 10, maxHpBonus: 20 } as any, - }, - }); - } else { - await prisma.itemMutation.create({ - data: { - key: "emerald_core", - name: "Núcleo de Esmeralda", - guildId, - effects: { defenseBonus: 10, maxHpBonus: 20 } as any, - } as any, - }); - } - - // --- Recipes (crafteo): iron_ingot <- iron ore x3 - // Create ingredient items if missing - await upsertEconomyItem(guildId, "ingot.iron", { - name: "Lingote de Hierro", - stackable: true, - props: {} as unknown as Prisma.InputJsonValue, - tags: ["ingot"], - }); - await upsertEconomyItem(guildId, "ore.iron", { - name: "Mineral de Hierro", - stackable: true, - props: {} as unknown as Prisma.InputJsonValue, - tags: ["ore"], - }); - await upsertItemRecipe( - guildId, - "ingot.iron", - [{ itemKey: "ore.iron", qty: 3 }], - 1 - ); - - // --- Areas & Levels --- - const mine = await upsertGameArea(guildId, "mine.cavern", { - name: "Mina: Caverna", - type: "MINE", - config: { cooldownSeconds: 10 } as unknown as Prisma.InputJsonValue, - }); - await prisma.gameAreaLevel.upsert({ - where: { areaId_level: { areaId: mine.id, level: 1 } }, - update: {}, - create: { - areaId: mine.id, - level: 1, - requirements: { - tool: { required: true, toolType: "pickaxe", minTier: 1 }, - } as any, - rewards: { - draws: 2, - table: [ - { type: "item", itemKey: "ore.iron", qty: 2, weight: 70 }, - { type: "coins", amount: 10, weight: 30 }, - ], - } as any, - mobs: { - draws: 1, - table: [ - { mobKey: "slime.green", weight: 50 }, - { mobKey: "bat", weight: 50 }, - ], - } as any, - }, - }); - - const lagoon = await upsertGameArea(guildId, "lagoon.shore", { - name: "Laguna: Orilla", - type: "LAGOON", - config: { cooldownSeconds: 12 } as unknown as Prisma.InputJsonValue, - }); - await prisma.gameAreaLevel.upsert({ - where: { areaId_level: { areaId: lagoon.id, level: 1 } }, - update: {}, - create: { - areaId: lagoon.id, - level: 1, - requirements: { - tool: { required: true, toolType: "rod", minTier: 1 }, - } as any, - rewards: { - draws: 2, - table: [ - { type: "item", itemKey: "food.meat.small", qty: 1, weight: 70 }, - { type: "coins", amount: 10, weight: 30 }, - ], - } as any, - mobs: { draws: 0, table: [] } as any, - }, - }); - - // --- Basic mobs --- - await upsertMob(guildId, "slime.green", { - name: "Slime Verde", - stats: { attack: 4, hp: 18 } as any, - drops: [{ itemKey: "ingot.iron", qty: 1, weight: 10 }] as any, - }); - await upsertMob(guildId, "bat", { - name: "Murciélago", - stats: { attack: 3, hp: 10 } as any, - drops: Prisma.DbNull, - }); - - // Advanced mobs - await upsertMob(guildId, "goblin", { - name: "Duende", - stats: { attack: 8, hp: 30 } as any, - drops: [{ itemKey: "ore.iron", qty: 1, weight: 50 }] as any, - }); - await upsertMob(guildId, "orc", { - name: "Orco", - stats: { attack: 12, hp: 50 } as any, - drops: Prisma.DbNull, - }); - - // Programar un par de ataques demo (opcional) - const targetUser = process.env.TARGET_USER ?? null; - if (targetUser) { - const slime = await prisma.mob.findFirst({ - where: { key: "slime.green", OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); - if (slime) { - const now = Date.now(); - await prisma.scheduledMobAttack.createMany({ - data: [ - { - userId: targetUser, - guildId: guildId ?? "global", - mobId: slime.id, - scheduleAt: new Date(now + 5_000), - }, - { - userId: targetUser, - guildId: guildId ?? "global", - mobId: slime.id, - scheduleAt: new Date(now + 15_000), - }, - ], - }); - } - } - - console.log("Full server setup complete."); -} - -// Backwards-compatible CLI entry -if (require.main === module) { - const gid = process.env.GUILD_ID ?? DEFAULT_GUILD; - runFullServerSetup(gid) - .then(() => process.exit(0)) - .catch((e) => { - console.error(e); - process.exit(1); - }); -} diff --git a/scripts/migrateStackableToInstanced.ts b/scripts/migrateStackableToInstanced.ts deleted file mode 100644 index 840d54a..0000000 --- a/scripts/migrateStackableToInstanced.ts +++ /dev/null @@ -1,253 +0,0 @@ -/** - * Script de Migración: Stackable Items → Instanced Items con Durabilidad - * - * Problema: - * - Items de herramientas/armas en DB tienen stackable=true (error de versión antigua) - * - Inventarios tienen quantity>1 sin state.instances con durabilidad - * - Esto causa que reduceToolDurability decremente quantity en lugar de degradar durabilidad - * - * Solución: - * 1. Actualizar EconomyItem: stackable=false para tools/weapons/armor/capes - * 2. Migrar InventoryEntry: convertir quantity a state.instances[] con durabilidad inicializada - */ - -import { PrismaClient, Prisma } from "@prisma/client"; - -const prisma = new PrismaClient(); - -type ItemProps = { - breakable?: { - enabled?: boolean; - maxDurability?: number; - durabilityPerUse?: number; - }; - tool?: { type: string; tier?: number }; - damage?: number; - defense?: number; - [k: string]: unknown; -}; - -type InventoryState = { - instances?: Array<{ - durability?: number; - expiresAt?: string; - notes?: string; - mutations?: string[]; - }>; - notes?: string; - [k: string]: unknown; -}; - -async function main() { - console.log("🔧 Iniciando migración de items stackable...\n"); - - // PASO 1: Actualizar definiciones de items - console.log("📝 PASO 1: Actualizando EconomyItem (stackable → false)..."); - const itemUpdateResult = await prisma.$executeRaw` - UPDATE "EconomyItem" - SET "stackable" = false - WHERE "key" LIKE 'tool.%' - OR "key" LIKE 'weapon.%' - OR "key" LIKE 'armor.%' - OR "key" LIKE 'cape.%' - `; - console.log(`✅ ${itemUpdateResult} items actualizados\n`); - - // PASO 2: Obtener items que ahora son non-stackable - const nonStackableItems = await prisma.economyItem.findMany({ - where: { - stackable: false, - OR: [ - { key: { startsWith: "tool." } }, - { key: { startsWith: "weapon." } }, - { key: { startsWith: "armor." } }, - { key: { startsWith: "cape." } }, - ], - }, - }); - - console.log( - `📦 ${nonStackableItems.length} items non-stackable identificados\n` - ); - - // PASO 3: Migrar inventarios - console.log("🔄 PASO 2: Migrando inventarios..."); - - let migratedCount = 0; - let skippedCount = 0; - let errorCount = 0; - - for (const item of nonStackableItems) { - const props = (item.props as ItemProps | null) ?? {}; - const breakable = props.breakable; - const maxDurability = - breakable?.enabled !== false - ? breakable?.maxDurability ?? 100 - : undefined; - - // Encontrar todas las entradas de inventario de este item con quantity>1 o sin instances - const entries = await prisma.inventoryEntry.findMany({ - where: { itemId: item.id }, - }); - - for (const entry of entries) { - try { - const currentState = (entry.state as InventoryState | null) ?? {}; - const currentInstances = currentState.instances ?? []; - const currentQuantity = entry.quantity ?? 0; - - // Caso 1: quantity>1 pero sin instances (inventario corrupto de versión anterior) - if (currentQuantity > 1 && currentInstances.length === 0) { - console.log( - ` 🔧 Migrando: ${item.key} (user=${entry.userId.slice( - 0, - 8 - )}, qty=${currentQuantity})` - ); - - const newInstances: InventoryState["instances"] = []; - for (let i = 0; i < currentQuantity; i++) { - if (maxDurability && maxDurability > 0) { - newInstances.push({ durability: maxDurability }); - } else { - newInstances.push({}); - } - } - - await prisma.inventoryEntry.update({ - where: { id: entry.id }, - data: { - state: { - ...currentState, - instances: newInstances, - } as unknown as Prisma.InputJsonValue, - quantity: newInstances.length, - }, - }); - - migratedCount++; - } - // Caso 2: Instancia única sin durabilidad inicializada - else if (currentQuantity === 1 && currentInstances.length === 0) { - const newInstance = - maxDurability && maxDurability > 0 - ? { durability: maxDurability } - : {}; - - await prisma.inventoryEntry.update({ - where: { id: entry.id }, - data: { - state: { - ...currentState, - instances: [newInstance], - } as unknown as Prisma.InputJsonValue, - quantity: 1, - }, - }); - - migratedCount++; - } - // Caso 3: Ya tiene instances pero sin durabilidad inicializada - else if (currentInstances.length > 0 && maxDurability) { - let needsUpdate = false; - const fixedInstances = currentInstances.map((inst) => { - if (inst.durability == null) { - needsUpdate = true; - return { ...inst, durability: maxDurability }; - } - return inst; - }); - - if (needsUpdate) { - console.log( - ` 🔧 Reparando durabilidad: ${ - item.key - } (user=${entry.userId.slice(0, 8)}, instances=${ - fixedInstances.length - })` - ); - await prisma.inventoryEntry.update({ - where: { id: entry.id }, - data: { - state: { - ...currentState, - instances: fixedInstances, - } as unknown as Prisma.InputJsonValue, - quantity: fixedInstances.length, - }, - }); - migratedCount++; - } else { - skippedCount++; - } - } else { - skippedCount++; - } - } catch (error) { - console.error(` ❌ Error migrando entry ${entry.id}:`, error); - errorCount++; - } - } - } - - console.log("\n📊 Resumen de migración:"); - console.log(` ✅ Entradas migradas: ${migratedCount}`); - console.log(` ⏭️ Entradas omitidas (ya correctas): ${skippedCount}`); - console.log(` ❌ Errores: ${errorCount}\n`); - - // PASO 4: Validación post-migración - console.log("🔍 PASO 3: Validando integridad..."); - const inconsistentEntries = await prisma.$queryRaw< - Array<{ - id: string; - userId: string; - key: string; - quantity: number; - state: any; - }> - >` - SELECT - ie.id, - ie."userId", - ei.key, - ie.quantity, - ie.state - FROM "InventoryEntry" ie - JOIN "EconomyItem" ei ON ie."itemId" = ei.id - WHERE ei."stackable" = false - AND ie.quantity > 1 - AND ( - ie.state IS NULL - OR jsonb_array_length(COALESCE((ie.state->>'instances')::jsonb, '[]'::jsonb)) = 0 - ) - `; - - if (inconsistentEntries.length > 0) { - console.log( - `\n⚠️ ADVERTENCIA: ${inconsistentEntries.length} entradas inconsistentes detectadas:` - ); - inconsistentEntries.forEach((entry) => { - console.log( - ` - ${entry.key} (user=${entry.userId.slice(0, 8)}, qty=${ - entry.quantity - })` - ); - }); - console.log( - "\n❗ Ejecuta el comando admin !reset-inventory para estos usuarios\n" - ); - } else { - console.log("✅ No se detectaron inconsistencias\n"); - } - - console.log("🎉 Migración completada exitosamente"); -} - -main() - .catch((error) => { - console.error("❌ Error fatal durante migración:", error); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); diff --git a/scripts/mobAdminTest.ts b/scripts/mobAdminTest.ts deleted file mode 100644 index 7a71faf..0000000 --- a/scripts/mobAdminTest.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - createOrUpdateMob, - listMobs, - getMob, - deleteMob, - ensureMobRepoUpToDate, -} from "../src/game/mobs/admin"; - -async function run() { - console.log("Ensuring repo up-to-date..."); - await ensureMobRepoUpToDate(); - - const testMob = { - key: "test.goblin", - name: "Goblin Test", - tier: 1, - base: { hp: 12, attack: 3 }, - } as any; - - console.log("Creating test mob..."); - const created = await createOrUpdateMob(testMob); - console.log("Created:", created.key); - - console.log("Listing mobs (sample):"); - const all = await listMobs(); - console.log(`Total mobs: ${all.length}`); - console.log(all.map((m) => m.key).join(", ")); - - console.log("Fetching test.mob..."); - const fetched = await getMob("test.goblin"); - console.log("Fetched:", !!fetched, fetched ? fetched : "(no data)"); - - console.log("Deleting test mob..."); - const deleted = await deleteMob("test.goblin"); - console.log("Deleted?", deleted); -} - -run().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/scripts/removeInvalidMobsWithDeps.ts b/scripts/removeInvalidMobsWithDeps.ts deleted file mode 100644 index 4a5d350..0000000 --- a/scripts/removeInvalidMobsWithDeps.ts +++ /dev/null @@ -1,72 +0,0 @@ -import fs from "fs"; -import { prisma } from "../src/core/database/prisma"; - -async function run() { - if (!process.env.XATA_DB) { - console.error("XATA_DB not set — aborting"); - process.exit(1); - } - - if (!fs.existsSync("invalid_mobs_backup.json")) { - console.error("invalid_mobs_backup.json not found — run cleanup first"); - process.exit(1); - } - - const bak = JSON.parse(fs.readFileSync("invalid_mobs_backup.json", "utf8")); - const ids: string[] = bak.map((b: any) => b.id).filter(Boolean); - if (ids.length === 0) { - console.log("No ids found in invalid_mobs_backup.json"); - return; - } - - console.log( - "Backing up ScheduledMobAttack rows that reference these mob ids..." - ); - try { - const deps = await (prisma as any).scheduledMobAttack.findMany({ - where: { mobId: { in: ids } }, - }); - fs.writeFileSync( - "scheduled_mob_attack_backup.json", - JSON.stringify(deps, null, 2) - ); - console.log( - `Backed up ${deps.length} ScheduledMobAttack rows to scheduled_mob_attack_backup.json` - ); - - if (deps.length > 0) { - console.log( - "Deleting ScheduledMobAttack rows referencing invalid mobs..." - ); - const delRes = await (prisma as any).scheduledMobAttack.deleteMany({ - where: { mobId: { in: ids } }, - }); - console.log(`Deleted ${delRes.count || delRes} ScheduledMobAttack rows`); - } else { - console.log("No dependent ScheduledMobAttack rows to delete."); - } - } catch (e: any) { - console.error("Failed to backup/delete dependent rows:", e?.message ?? e); - process.exit(1); - } - - console.log("Deleting invalid mob rows from Mob table..."); - try { - const delMobs = await (prisma as any).mob.deleteMany({ - where: { id: { in: ids } }, - }); - console.log(`Deleted ${delMobs.count || delMobs} mob rows`); - } catch (e: any) { - console.error("Failed to delete mob rows:", e?.message ?? e); - process.exit(1); - } - - console.log( - "Done. Backups: invalid_mobs_backup.json, scheduled_mob_attack_backup.json" - ); -} - -run().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/scripts/setupFeatureFlags.ts b/scripts/setupFeatureFlags.ts deleted file mode 100644 index f057bad..0000000 --- a/scripts/setupFeatureFlags.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Setup inicial del sistema de Feature Flags - * - * Este script: - * 1. Crea algunos feature flags de ejemplo - * 2. Muestra cómo configurar diferentes estrategias - */ - -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); - -async function setupFeatureFlags() { - console.log("🎮 Configurando Feature Flags de ejemplo...\n"); - - try { - // 1. Flag deshabilitado por defecto (en desarrollo) - await prisma.featureFlag.upsert({ - where: { name: "new_shop_system" }, - create: { - name: "new_shop_system", - description: "Nuevo sistema de tienda con UI mejorada", - status: "disabled", - target: "global", - }, - update: {}, - }); - console.log('✅ Flag "new_shop_system" creado (disabled)'); - - // 2. Flag habilitado globalmente - await prisma.featureFlag.upsert({ - where: { name: "inventory_ui_v2" }, - create: { - name: "inventory_ui_v2", - description: "Nueva UI del inventario con mejor UX", - status: "enabled", - target: "global", - }, - update: {}, - }); - console.log('✅ Flag "inventory_ui_v2" creado (enabled)'); - - // 3. Flag con rollout por porcentaje (25% de usuarios) - await prisma.featureFlag.upsert({ - where: { name: "improved_combat_algorithm" }, - create: { - name: "improved_combat_algorithm", - description: "Algoritmo de combate mejorado con mejor balance", - status: "rollout", - target: "user", - rolloutStrategy: "percentage", - rolloutConfig: JSON.stringify({ - percentage: 25, - }), - }, - update: {}, - }); - console.log('✅ Flag "improved_combat_algorithm" creado (rollout 25%)'); - - // 4. Flag con rollout gradual (de 10% a 100% en 7 días) - const startDate = new Date(); - await prisma.featureFlag.upsert({ - where: { name: "economy_system_v2" }, - create: { - name: "economy_system_v2", - description: "Sistema de economía rediseñado", - status: "rollout", - target: "user", - rolloutStrategy: "gradual", - rolloutConfig: JSON.stringify({ - gradual: { - startPercentage: 10, - targetPercentage: 100, - durationDays: 7, - }, - }), - startDate, - }, - update: {}, - }); - console.log('✅ Flag "economy_system_v2" creado (gradual rollout)'); - - // 5. Flag de evento temporal (Halloween) - const halloweenStart = new Date("2025-10-25"); - const halloweenEnd = new Date("2025-11-01"); - await prisma.featureFlag.upsert({ - where: { name: "halloween_2025" }, - create: { - name: "halloween_2025", - description: "Evento de Halloween 2025", - status: "enabled", - target: "global", - startDate: halloweenStart, - endDate: halloweenEnd, - }, - update: {}, - }); - console.log('✅ Flag "halloween_2025" creado (evento temporal)'); - - // 6. Flag experimental con whitelist (solo para beta testers) - await prisma.featureFlag.upsert({ - where: { name: "experimental_features" }, - create: { - name: "experimental_features", - description: "Funcionalidades experimentales solo para beta testers", - status: "rollout", - target: "user", - rolloutStrategy: "whitelist", - rolloutConfig: JSON.stringify({ - targetIds: [ - // Añade aquí los IDs de tus beta testers - "BETA_TESTER_USER_ID_1", - "BETA_TESTER_USER_ID_2", - ], - }), - }, - update: {}, - }); - console.log('✅ Flag "experimental_features" creado (whitelist)'); - - // 7. Flag premium con múltiples requisitos - await prisma.featureFlag.upsert({ - where: { name: "premium_features" }, - create: { - name: "premium_features", - description: "Funcionalidades premium del bot", - status: "disabled", - target: "guild", - metadata: JSON.stringify({ - requiresSubscription: true, - tier: "premium", - }), - }, - update: {}, - }); - console.log('✅ Flag "premium_features" creado (disabled, metadata)'); - - // 8. Flag en mantenimiento - await prisma.featureFlag.upsert({ - where: { name: "trading_system" }, - create: { - name: "trading_system", - description: "Sistema de intercambio entre usuarios", - status: "maintenance", - target: "global", - }, - update: {}, - }); - console.log('✅ Flag "trading_system" creado (maintenance)'); - - console.log("\n🎉 Feature Flags de ejemplo creados exitosamente!"); - console.log("\n📝 Próximos pasos:"); - console.log("1. Usa /featureflags list para ver todos los flags"); - console.log("2. Usa /featureflags info flag:nombre para ver detalles"); - console.log( - "3. Usa /featureflags update flag:nombre status:enabled para habilitar" - ); - console.log("4. Lee README/FEATURE_FLAGS_SYSTEM.md para más información"); - } catch (error) { - console.error("❌ Error al crear flags:", error); - throw error; - } finally { - await prisma.$disconnect(); - } -} - -// Ejecutar -setupFeatureFlags() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); diff --git a/scripts/setupGuildCacheCollection.js b/scripts/setupGuildCacheCollection.js deleted file mode 100644 index e71b006..0000000 --- a/scripts/setupGuildCacheCollection.js +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env node -/** - * Script para crear automáticamente la colección de caché de guilds en Appwrite - * - * Uso: - * node scripts/setupGuildCacheCollection.js - * - * Requisitos: - * - Tener las variables de entorno de Appwrite configuradas - * - Tener node-appwrite instalado - */ -process.loadEnvFile(); -const { Client, Databases, Permission, Role } = require('node-appwrite'); - -const COLLECTION_NAME = 'guild_cache_id'; - -async function setup() { - console.log('🚀 Configurando colección de caché de guilds en Appwrite...\n'); - - // Validar variables de entorno - const endpoint = process.env.APPWRITE_ENDPOINT; - const projectId = process.env.APPWRITE_PROJECT_ID; - const apiKey = process.env.APPWRITE_API_KEY; - const databaseId = process.env.APPWRITE_DATABASE_ID; - - if (!endpoint || !projectId || !apiKey || !databaseId) { - console.error('❌ Error: Faltan variables de entorno de Appwrite'); - console.error(' Asegúrate de tener configurado:'); - console.error(' - APPWRITE_ENDPOINT'); - console.error(' - APPWRITE_PROJECT_ID'); - console.error(' - APPWRITE_API_KEY'); - console.error(' - APPWRITE_DATABASE_ID'); - process.exit(1); - } - - // Inicializar cliente - const client = new Client() - .setEndpoint(endpoint) - .setProject(projectId) - .setKey(apiKey); - - const databases = new Databases(client); - - try { - // 1. Crear colección - console.log('📦 Creando colección...'); - const collection = await databases.createCollection( - databaseId, - 'unique()', // ID autogenerado - COLLECTION_NAME, - [ - Permission.read(Role.any()), - Permission.create(Role.any()), - Permission.update(Role.any()), - Permission.delete(Role.any()) - ] - ); - - console.log(`✅ Colección creada: ${collection.$id}\n`); - const collectionId = collection.$id; - - // 2. Crear atributo guildId (string, required, unique) - console.log('📝 Creando atributo: guildId'); - await databases.createStringAttribute( - databaseId, - collectionId, - 'guildId', - 32, - true, // required - null, - false, - false - ); - console.log('✅ Atributo guildId creado'); - - // 3. Crear atributo name (string, required) - console.log('📝 Creando atributo: name'); - await databases.createStringAttribute( - databaseId, - collectionId, - 'name', - 100, - true, // required - null, - false, - false - ); - console.log('✅ Atributo name creado'); - - // 4. Crear atributo prefix (string, optional) - console.log('📝 Creando atributo: prefix'); - await databases.createStringAttribute( - databaseId, - collectionId, - 'prefix', - 10, - false, // not required - null, - false, - false - ); - console.log('✅ Atributo prefix creado'); - - // 5. Crear atributo expiresAt (datetime, required) - console.log('📝 Creando atributo: expiresAt'); - await databases.createDatetimeAttribute( - databaseId, - collectionId, - 'expiresAt', - true, // required - null, - false, - false - ); - console.log('✅ Atributo expiresAt creado'); - - // Esperar un poco para que Appwrite procese los atributos - console.log('\n⏳ Esperando 5 segundos para que los atributos se procesen...'); - await new Promise(resolve => setTimeout(resolve, 5000)); - - // 6. Crear índice en guildId (unique) - console.log('📝 Creando índice único en guildId'); - await databases.createIndex( - databaseId, - collectionId, - 'idx_guildId', - 'unique', - ['guildId'], - ['ASC'] - ); - console.log('✅ Índice en guildId creado'); - - // 7. Crear índice en expiresAt (para queries de limpieza) - console.log('📝 Creando índice en expiresAt'); - await databases.createIndex( - databaseId, - collectionId, - 'idx_expiresAt', - 'key', - ['expiresAt'], - ['ASC'] - ); - console.log('✅ Índice en expiresAt creado'); - - console.log('\n🎉 ¡Configuración completada exitosamente!'); - console.log('\n📋 Agrega esta variable a tu .env:'); - console.log(`APPWRITE_COLLECTION_GUILD_CACHE_ID=${collectionId}`); - console.log('\n💡 Recuerda reiniciar tu bot después de agregar la variable.'); - - } catch (error) { - console.error('\n❌ Error durante la configuración:', error.message); - if (error.response) { - console.error('Detalles:', error.response); - } - process.exit(1); - } -} - -setup(); diff --git a/scripts/smokeQuestsAchievements.ts b/scripts/smokeQuestsAchievements.ts deleted file mode 100644 index e7ca941..0000000 --- a/scripts/smokeQuestsAchievements.ts +++ /dev/null @@ -1,99 +0,0 @@ -import "dotenv/config"; -import { prisma } from "../src/core/database/prisma"; -import { seedAchievements } from "../src/game/achievements/seed"; -import { - generateDailyQuests, - updateQuestProgress, - claimQuestReward, - getPlayerQuests, -} from "../src/game/quests/service"; -import { - checkAchievements, - getPlayerAchievements, -} from "../src/game/achievements/service"; - -async function ensureGuildAndUser(guildId: string, userId: string) { - await prisma.guild.upsert({ - where: { id: guildId }, - update: { name: "test" }, - create: { id: guildId, name: "test", prefix: "!" }, - }); - await prisma.user.upsert({ - where: { id: userId }, - update: {}, - create: { id: userId }, - }); -} - -async function run() { - const guildId = "test-guild-quests"; - const userId = "test-user-1"; - await ensureGuildAndUser(guildId, userId); - - console.log("Seeding achievements global..."); - await seedAchievements(null); - - console.log("Seeding achievements for guild..."); - await seedAchievements(guildId); - - console.log("Generating daily quests for guild..."); - await generateDailyQuests(guildId); - - console.log("Player quests before progress:"); - console.log(await getPlayerQuests(userId, guildId)); - - // Buscar una quest de tipo fight_count - const quests = await prisma.quest.findMany({ - where: { guildId, type: "daily" }, - }); - const q = quests.find((q) => (q.requirements as any).type === "fight_count"); - if (!q) { - console.log("No daily fight quest found, picking first."); - } - const questToUse = q || quests[0]; - console.log("Using quest:", questToUse?.key); - - // Simular progreso para completarla - const req = (questToUse.requirements as any) || { count: 1 }; - const needed = req.count || 1; - console.log("Incrementing progress by", needed); - const updates = await updateQuestProgress( - userId, - guildId, - req.type || "fight_count", - needed - ); - console.log( - "Quests completed by updateQuestProgress:", - updates.map((u) => u.key) - ); - - // Intentar reclamar - const progressRow = await prisma.questProgress.findFirst({ - where: { userId, guildId, questId: questToUse.id, completed: true }, - }); - if (progressRow) { - const claim = await claimQuestReward(userId, guildId, questToUse.id); - console.log("Claim result:", claim); - } else { - console.log("No completed quest progress to claim"); - } - - // Check achievements trigger - console.log("Checking achievements for fight_count"); - const unlocked = await checkAchievements(userId, guildId, "fight_count"); - console.log( - "Achievements unlocked:", - unlocked.map((a) => a.key) - ); - - console.log("Player achievements summary:"); - console.log(await getPlayerAchievements(userId, guildId)); - - process.exit(0); -} - -run().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/scripts/testCreateFlag.ts b/scripts/testCreateFlag.ts deleted file mode 100644 index b614f26..0000000 --- a/scripts/testCreateFlag.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Test de creación de flag específico - */ - -import { featureFlagService } from "../src/core/services/FeatureFlagService"; -import { FeatureFlagConfig } from "../src/core/types/featureFlags"; - -async function testCreateFlag() { - console.log("🧪 Test: Crear flag problemático\n"); - - try { - await featureFlagService.initialize(); - console.log("✅ Servicio inicializado\n"); - - const testFlag: FeatureFlagConfig = { - name: "2025-10-alianzas-blacklist", - description: "Blacklist de alianzas para octubre 2025", - status: "disabled", - target: "global", - }; - - console.log("📝 Intentando crear flag:", testFlag.name); - await featureFlagService.setFlag(testFlag); - console.log("✅ Flag creado exitosamente"); - - // Verificar - const flag = featureFlagService.getFlag(testFlag.name); - console.log("\n📊 Flag guardado:", flag); - - // Limpieza - await featureFlagService.removeFlag(testFlag.name); - console.log("\n🗑️ Flag eliminado"); - } catch (error: any) { - console.error("\n❌ ERROR:"); - console.error("Message:", error?.message); - console.error("Stack:", error?.stack); - console.error("Code:", error?.code); - console.error("Meta:", error?.meta); - console.error("Full error:", error); - } -} - -testCreateFlag(); diff --git a/scripts/testDiscordCommandFlow.ts b/scripts/testDiscordCommandFlow.ts deleted file mode 100644 index 13a567e..0000000 --- a/scripts/testDiscordCommandFlow.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Test que simula el flujo completo del comando /featureflags create - * para reproducir el error "Cannot read properties of undefined (reading 'upsert')" - */ - -import { featureFlagService } from "../src/core/services/FeatureFlagService"; -import { FeatureFlagConfig } from "../src/core/types/featureFlags"; - -async function simulateDiscordCommand() { - console.log("🎮 Simulando comando /featureflags create desde Discord\n"); - - try { - // Paso 1: Inicializar servicio (como lo haría el bot al arrancar) - console.log("1️⃣ Inicializando servicio (bot startup)..."); - await featureFlagService.initialize(); - console.log("✅ Servicio inicializado\n"); - - // Paso 2: Simular un delay (como si el bot ya estuviera corriendo) - console.log("2️⃣ Esperando 500ms (simulando bot en runtime)..."); - await new Promise((resolve) => setTimeout(resolve, 500)); - console.log("✅ Delay completado\n"); - - // Paso 3: Simular el comando /featureflags create (handleCreate) - console.log("3️⃣ Ejecutando handleCreate (como en el comando Discord)..."); - - const config: FeatureFlagConfig = { - name: "2025-10-alianza-blacklist", // Mismo nombre del error - description: "Test flag desde comando Discord", - status: "disabled", - target: "global", - }; - - console.log(" Llamando a featureFlagService.setFlag()..."); - await featureFlagService.setFlag(config); - console.log("✅ Flag creado exitosamente\n"); - - // Paso 4: Verificar - console.log("4️⃣ Verificando flag..."); - const flag = featureFlagService.getFlag(config.name); - console.log(" Flag:", flag); - console.log("✅ Verificación completa\n"); - - // Paso 5: Cleanup - console.log("5️⃣ Limpiando..."); - await featureFlagService.removeFlag(config.name); - console.log("✅ Flag eliminado\n"); - - console.log("🎉 Test completado sin errores"); - } catch (error: any) { - console.error("\n❌ ERROR CAPTURADO:"); - console.error("Message:", error?.message); - console.error("Stack:", error?.stack); - console.error("Code:", error?.code); - console.error("\nTipo de error:", error?.constructor?.name); - - // Diagnóstico adicional - console.error("\n🔍 Diagnóstico adicional:"); - try { - const { prisma } = await import("../src/core/database/prisma"); - console.error(" prisma:", typeof prisma); - console.error( - " prisma.featureFlag:", - typeof (prisma as any).featureFlag - ); - console.error( - " Keys de prisma:", - Object.keys(prisma as any).slice(0, 30) - ); - } catch (diagError) { - console.error( - " No se pudo acceder a prisma para diagnóstico:", - diagError - ); - } - - process.exit(1); - } -} - -simulateDiscordCommand(); diff --git a/scripts/testMobData.ts b/scripts/testMobData.ts deleted file mode 100644 index 25efe2a..0000000 --- a/scripts/testMobData.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - initializeMobRepository, - getMobInstance, - listMobKeys, -} from "../src/game/mobs/mobData"; - -async function run() { - console.log("Initializing mob repository..."); - await initializeMobRepository(); - console.log("Available mob keys:", listMobKeys()); - const inst = getMobInstance("slime.green", 3); - console.log("Sample slime.green @ lvl3 ->", inst); -} - -run().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/scripts/testMobUnit.ts b/scripts/testMobUnit.ts deleted file mode 100644 index 6c05197..0000000 --- a/scripts/testMobUnit.ts +++ /dev/null @@ -1,54 +0,0 @@ -import assert from "assert"; -import { - computeMobStats, - getMobInstance, - MOB_DEFINITIONS, -} from "../src/game/mobs/mobData"; -import { createOrUpdateMob } from "../src/game/mobs/admin"; - -async function run() { - console.log("Running mob unit tests..."); - - // computeMobStats basic - const def = MOB_DEFINITIONS[0]; - const statsLv1 = computeMobStats(def, 1); - assert(typeof statsLv1.hp === "number", "hp should be number"); - assert(typeof statsLv1.attack === "number", "attack should be number"); - - // scaling test - const statsLv5 = computeMobStats(def, 5); - if ((def.scaling && def.scaling.hpPerLevel) || 0) { - assert(statsLv5.hp >= statsLv1.hp, "hp should not decrease with level"); - } - - console.log("computeMobStats: OK"); - - // getMobInstance basic - const key = def.key; - const inst = getMobInstance(key, 3); - assert(inst !== null, "getMobInstance should return instance"); - assert(inst!.scaled.hp > 0, "instance hp > 0"); - console.log("getMobInstance: OK"); - - // createOrUpdateMob (non-DB mode should return def) - try { - const res = await createOrUpdateMob({ - ...def, - key: "unit.test.mob", - } as any); - if (!res || !res.def) throw new Error("createOrUpdateMob returned invalid"); - console.log("createOrUpdateMob: OK (no-DB mode)"); - } catch (e) { - console.warn( - "createOrUpdateMob: skipped (DB may be required) -", - (e as any)?.message ?? e - ); - } - - console.log("All mob unit tests passed."); -} - -run().catch((e) => { - console.error("Tests failed:", e); - process.exit(1); -}); diff --git a/scripts/testRewardMods.ts b/scripts/testRewardMods.ts deleted file mode 100644 index 480bafe..0000000 --- a/scripts/testRewardMods.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - initializeMobRepository, - getMobInstance, - listMobKeys, -} from "../src/game/mobs/mobData"; -import { runMinigame } from "../src/game/minigames/service"; -import { prisma } from "../src/core/database/prisma"; - -async function run() { - console.log("Initializing mob repository..."); - await initializeMobRepository(); - console.log("Available mob keys:", listMobKeys()); - - // Mock user/guild for smoke (these should exist in your test DB or the functions will create wallet entries etc.) - const userId = "test-user"; - const guildId = "test-guild"; - - try { - console.log("Ensuring minimal game area 'mine.cavern' exists..."); - // create minimal area and level if not present - let area = await prisma.gameArea.findFirst({ - where: { key: "mine.cavern", OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); - if (!area) { - area = await prisma.gameArea.create({ - data: { - key: "mine.cavern", - guildId: null, - name: "Cavern of Tests", - type: "MINE", - config: {}, - metadata: {}, - } as any, - }); - } - let lvl = await prisma.gameAreaLevel.findFirst({ - where: { areaId: area.id, level: 1 }, - }); - if (!lvl) { - lvl = await prisma.gameAreaLevel.create({ - data: { - areaId: area.id, - level: 1, - requirements: {} as any, - rewards: { - draws: 1, - table: [{ type: "coins", amount: 5, weight: 1 }], - } as any, - mobs: { draws: 0, table: [] } as any, - } as any, - }); - } - - console.log("Running minigame mine.cavern level 1 as smoke test..."); - const res = await runMinigame(userId, guildId, "mine.cavern", 1); - console.log("Minigame result:", JSON.stringify(res, null, 2)); - } catch (e) { - console.error("runMinigame failed:", e); - } -} - -run().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/scripts/test_render.js b/scripts/test_render.js deleted file mode 100644 index 9fe8225..0000000 --- a/scripts/test_render.js +++ /dev/null @@ -1,16 +0,0 @@ -(async()=>{ - try{ - const ejs=require('ejs'), path=require('path'); - const viewsDir=path.join(process.cwd(),'src','server','views'); - const pageFile = path.join(viewsDir,'pages','dashboard.ejs'); - const layoutFile = path.join(viewsDir,'layouts','layout.ejs'); - const locals={ appName:'amayo', user:{username:'test',id:'1',avatar:''}, guilds:[], useDashboardNav:false, version:'x', selectedGuild: null, title:'Dashboard test', hideNavbar:false }; - console.log('render page'); - const pageBody = await ejs.renderFile(pageFile, locals, {async:true, views:[viewsDir]}); - console.log('pageBody type:', typeof pageBody); - const html = await ejs.renderFile(layoutFile, {...locals, body: pageBody, dashboardNav:null, navbar:null, title: locals.title, useDashboardNav: locals.useDashboardNav}, {async:true, views:[viewsDir]}); - console.log('html length', html.length); - if (html.indexOf('[object Promise]')!==-1){ console.log('FOUND at', html.indexOf('[object Promise]')); console.log(html.slice(0,200)); process.exit(3);} - console.log('OK NO PROMISE'); process.exit(0); - }catch(e){ console.error('ERR',e); process.exit(2);} -})(); \ No newline at end of file diff --git a/src/.backup/admin/areaEliminar.ts b/src/.backup/admin/areaEliminar.ts deleted file mode 100644 index 3cb988a..0000000 --- a/src/.backup/admin/areaEliminar.ts +++ /dev/null @@ -1,104 +0,0 @@ -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 type { TextBasedChannel } from 'discord.js'; -import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2'; - -export const command: CommandMessage = { - name: 'area-eliminar', - type: 'message', - aliases: ['eliminar-area', 'area-delete'], - cooldown: 5, - description: 'Eliminar un área del servidor', - usage: 'area-eliminar ', - run: async (message, args, client: Amayo) => { - const channel = message.channel as TextBasedChannel & { send: Function }; - const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, prisma); - if (!allowed) { - await (channel.send as any)({ - content: null, - flags: 32768, - components: [ - buildDisplay(0xFF0000, [ - textBlock('❌ **Error de Permisos**\n└ No tienes permisos de ManageGuild ni rol de staff.') - ]) - ], - reply: { messageReference: message.id } - }); - return; - } - - const guildId = message.guild!.id; - const key = args[0]?.trim(); - - if (!key) { - await (channel.send as any)({ - content: null, - flags: 32768, - components: [ - buildDisplay(0xFFA500, [ - textBlock('⚠️ **Uso Incorrecto**'), - dividerBlock(), - textBlock('└ Uso: `!area-eliminar `\n└ Ejemplo: `!area-eliminar mine.cavern`') - ]) - ], - reply: { messageReference: message.id } - }); - return; - } - - const area = await prisma.gameArea.findFirst({ - where: { key, guildId } - }); - - if (!area) { - await (channel.send as any)({ - content: null, - flags: 32768, - components: [ - buildDisplay(0xFF0000, [ - textBlock(`❌ **Área No Encontrada**`), - dividerBlock(), - textBlock(`└ No se encontró el área local con key \`${key}\` en este servidor.`) - ]) - ], - reply: { messageReference: message.id } - }); - return; - } - - const levelsCount = await prisma.gameAreaLevel.count({ - where: { areaId: area.id } - }); - - if (levelsCount > 0) { - await prisma.gameAreaLevel.deleteMany({ - where: { areaId: area.id } - }); - } - - await prisma.gameArea.delete({ - where: { id: area.id } - }); - - await (channel.send as any)({ - content: null, - flags: 32768, - components: [ - buildDisplay(0x00FF00, [ - textBlock(`✅ **Área Eliminada Exitosamente**`), - dividerBlock(), - textBlock(`└ Key: \`${key}\``), - ...(levelsCount > 0 - ? [ - dividerBlock(), - textBlock(`⚠️ Se eliminaron ${levelsCount} nivel(es) asociado(s).`), - ] - : []), - ]) - ], - reply: { messageReference: message.id } - }); - } -}; diff --git a/src/.backup/admin/areasLista.ts b/src/.backup/admin/areasLista.ts deleted file mode 100644 index 1c69d20..0000000 --- a/src/.backup/admin/areasLista.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { prisma } from '../../../core/database/prisma'; -import { ComponentType, ButtonStyle } from 'discord-api-types/v10'; -import type { MessageComponentInteraction, TextBasedChannel } from 'discord.js'; -import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2'; - -export const command: CommandMessage = { - name: 'areas-lista', - type: 'message', - aliases: ['lista-areas', 'areas-list'], - cooldown: 5, - description: 'Ver lista de todas las áreas del servidor', - usage: 'areas-lista [pagina]', - run: async (message, args, client: Amayo) => { - const guildId = message.guild!.id; - const page = parseInt(args[0]) || 1; - const perPage = 6; - - const total = await prisma.gameArea.count({ - where: { OR: [{ guildId }, { guildId: null }] } - }); - - const areas = await prisma.gameArea.findMany({ - where: { OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ key: 'asc' }], - skip: (page - 1) * perPage, - take: perPage - }); - - if (areas.length === 0) { - await message.reply('No hay áreas configuradas en este servidor.'); - return; - } - - const totalPages = Math.ceil(total / perPage); - - const displayBlocks = [ - textBlock(`# 🗺️ Lista de Áreas`), - dividerBlock(), - textBlock(`Página ${page}/${totalPages} • Total: ${total}`), - dividerBlock({ divider: false, spacing: 2 }), - ...areas.flatMap((area, index) => { - const lines = [ - `**${area.name || area.key}**`, - `└ Key: \`${area.key}\``, - `└ ${area.guildId === guildId ? '📍 Local' : '🌐 Global'}`, - ].join('\n'); - - const blocks = [textBlock(lines)]; - if (index < areas.length - 1) { - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - } - return blocks; - }) - ]; - - const display = buildDisplay(0x00FF00, displayBlocks); - - const buttons: any[] = []; - - if (page > 1) { - buttons.push({ - type: ComponentType.Button, - style: ButtonStyle.Secondary, - label: '◀ Anterior', - custom_id: `areas_prev_${page}` - }); - } - - if (page < totalPages) { - buttons.push({ - type: ComponentType.Button, - style: ButtonStyle.Secondary, - label: 'Siguiente ▶', - custom_id: `areas_next_${page}` - }); - } - - const channel = message.channel as TextBasedChannel & { send: Function }; - const msg = await (channel.send as any)({ - content: null, - flags: 32768, - reply: { messageReference: message.id }, - components: [ - display, - ...(buttons.length > 0 ? [{ - type: ComponentType.ActionRow, - components: buttons - }] : []) - ] - }); - - const collector = msg.createMessageComponentCollector({ - time: 5 * 60_000, - filter: (i) => i.user.id === message.author.id - }); - - collector.on('collect', async (i: MessageComponentInteraction) => { - if (!i.isButton()) return; - - if (i.customId.startsWith('areas_prev_')) { - const currentPage = parseInt(i.customId.split('_')[2]); - await i.deferUpdate(); - args[0] = String(currentPage - 1); - await command.run!(message, args, client); - collector.stop(); - } else if (i.customId.startsWith('areas_next_')) { - const currentPage = parseInt(i.customId.split('_')[2]); - await i.deferUpdate(); - args[0] = String(currentPage + 1); - await command.run!(message, args, client); - collector.stop(); - } - }); - } -}; diff --git a/src/.backup/admin/debugInv.ts b/src/.backup/admin/debugInv.ts deleted file mode 100644 index 810b8ba..0000000 --- a/src/.backup/admin/debugInv.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { prisma } from "../../../core/database/prisma"; - -export const command: CommandMessage = { - name: "debug-inv", - type: "message", - aliases: ["dinv"], - cooldown: 0, - category: "Admin", - description: "Muestra información detallada del inventario para debug.", - usage: "debug-inv [@user]", - run: async (message, args, _client: Amayo) => { - if (message.author.id !== process.env.OWNER_ID) { - await message.reply("❌ Solo el owner puede usar este comando."); - return; - } - - const targetUser = message.mentions.users.first() ?? message.author; - const userId = targetUser.id; - const guildId = message.guild!.id; - - const entries = await prisma.inventoryEntry.findMany({ - where: { userId, guildId }, - include: { item: true }, - }); - - let output = `🔍 **Inventario de <@${userId}>**\n\n`; - - for (const entry of entries) { - const item = entry.item; - const state = entry.state as any; - const instances = state?.instances ?? []; - const props = item.props as any; - - output += `**${item.name}** (\`${item.key}\`)\n`; - output += `• Stackable: ${item.stackable}\n`; - output += `• Quantity: ${entry.quantity}\n`; - output += `• Instances: ${instances.length}\n`; - - if (props?.tool) { - output += `• Tool: type=${props.tool.type}, tier=${ - props.tool.tier ?? 0 - }\n`; - } - - if (props?.breakable) { - output += `• Breakable: enabled=${ - props.breakable.enabled !== false - }, max=${props.breakable.maxDurability}\n`; - } - - if (instances.length > 0) { - instances.forEach((inst: any, idx: number) => { - output += ` └ [${idx}] dur: ${inst.durability ?? "N/A"}\n`; - }); - } - - if (!item.stackable && entry.quantity > 1 && instances.length === 0) { - output += `⚠️ **CORRUPTO**: Non-stackable con qty>1 sin instances\n`; - } - - output += "\n"; - } - - // Verificar equipo - const equipment = await prisma.playerEquipment.findUnique({ - where: { userId_guildId: { userId, guildId } }, - }); - - if (equipment) { - output += `🧰 **Equipo:**\n`; - if (equipment.weaponItemId) { - const weapon = await prisma.economyItem.findUnique({ - where: { id: equipment.weaponItemId }, - }); - output += `• Arma: ${weapon?.name ?? "Desconocida"}\n`; - } else { - output += `• Arma: ❌ NINGUNA\n`; - } - - if (equipment.armorItemId) { - const armor = await prisma.economyItem.findUnique({ - where: { id: equipment.armorItemId }, - }); - output += `• Armadura: ${armor?.name ?? "Desconocida"}\n`; - } - - if (equipment.capeItemId) { - const cape = await prisma.economyItem.findUnique({ - where: { id: equipment.capeItemId }, - }); - output += `• Capa: ${cape?.name ?? "Desconocida"}\n`; - } - } - - // Dividir en chunks si es muy largo - const chunks = output.match(/[\s\S]{1,1900}/g) ?? [output]; - for (const chunk of chunks) { - await message.reply(chunk); - } - }, -}; diff --git a/src/.backup/admin/fixDurability.ts b/src/.backup/admin/fixDurability.ts deleted file mode 100644 index f07c8d8..0000000 --- a/src/.backup/admin/fixDurability.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { prisma } from "../../../core/database/prisma"; - -export const command: CommandMessage = { - name: "fix-durability", - type: "message", - aliases: ["fixdur", "repair-tools"], - description: - "Regenera la durabilidad de items sin inicializar (migración para items antiguos)", - usage: "fix-durability [@usuario]", - run: async (message, args, _client: Amayo) => { - const guildId = message.guild!.id; - const mention = message.mentions.users.first(); - const targetUserId = mention?.id || args[0] || message.author.id; - - try { - const entries = await prisma.inventoryEntry.findMany({ - where: { userId: targetUserId, guildId }, - include: { item: true }, - }); - - let fixed = 0; - let skipped = 0; - - for (const entry of entries) { - // Solo items no apilables - if (entry.item.stackable) { - skipped++; - continue; - } - - const props = (entry.item.props as any) ?? {}; - const breakable = props.breakable; - - // Sin durabilidad configurada o deshabilitada - if (!breakable || breakable.enabled === false) { - skipped++; - continue; - } - - const maxDurability = Math.max(1, breakable.maxDurability ?? 100); - const state = (entry.state as any) ?? {}; - const instances = Array.isArray(state.instances) ? state.instances : []; - - let needsFix = false; - const regenerated = instances.map((inst: any) => { - if ( - inst.durability == null || - typeof inst.durability !== "number" || - inst.durability <= 0 - ) { - needsFix = true; - return { ...inst, durability: maxDurability }; - } - return inst; - }); - - // Si no hay instancias pero quantity > 0, crearlas - if (regenerated.length === 0 && entry.quantity > 0) { - for (let i = 0; i < entry.quantity; i++) { - regenerated.push({ durability: maxDurability }); - } - needsFix = true; - } - - if (needsFix) { - await prisma.inventoryEntry.update({ - where: { id: entry.id }, - data: { - state: { ...state, instances: regenerated } as any, - quantity: regenerated.length, - }, - }); - fixed++; - } else { - skipped++; - } - } - - if (fixed === 0) { - await message.reply( - `✅ Todos los items de <@${targetUserId}> ya tienen durabilidad correcta (${skipped} items revisados).` - ); - } else { - await message.reply( - `🔧 Regeneradas **${fixed}** herramientas para <@${targetUserId}>. (${skipped} items no requerían fix)` - ); - } - } catch (e: any) { - await message.reply( - `❌ Error al regenerar durabilidad: ${e?.message ?? e}` - ); - } - }, -}; diff --git a/src/.backup/admin/itemEliminar.ts b/src/.backup/admin/itemEliminar.ts deleted file mode 100644 index 0b1ee39..0000000 --- a/src/.backup/admin/itemEliminar.ts +++ /dev/null @@ -1,64 +0,0 @@ -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'; - -export const command: CommandMessage = { - name: 'item-eliminar', - type: 'message', - aliases: ['eliminar-item', 'item-delete'], - cooldown: 5, - description: 'Eliminar un item del servidor', - usage: 'item-eliminar ', - 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 guildId = message.guild!.id; - const key = args[0]?.trim(); - - if (!key) { - await message.reply('Uso: `!item-eliminar `\nEjemplo: `!item-eliminar tool.pickaxe.iron`'); - return; - } - - const item = await prisma.economyItem.findFirst({ - where: { key, guildId } - }); - - if (!item) { - await message.reply(`❌ No se encontró el item local con key \`${key}\` en este servidor.\n` + - `💡 Solo puedes eliminar items locales del servidor, no globales.`); - return; - } - - // Verificar si está en uso - const inInventory = await prisma.inventoryEntry.count({ - where: { itemId: item.id, quantity: { gt: 0 } } - }); - - const inOffers = await prisma.shopOffer.count({ - where: { itemId: item.id } - }); - - if (inInventory > 0 || inOffers > 0) { - await message.reply( - `⚠️ **Advertencia:** Este item está en uso:\n` + - `${inInventory > 0 ? `• En ${inInventory} inventario(s)\n` : ''}` + - `${inOffers > 0 ? `• En ${inOffers} oferta(s) de tienda\n` : ''}` + - `¿Estás seguro? Usa \`!item-eliminar-forzar ${key}\` para confirmar.` - ); - return; - } - - // Eliminar el item - await prisma.economyItem.delete({ - where: { id: item.id } - }); - - await message.reply(`✅ Item \`${key}\` eliminado exitosamente.`); - } -}; diff --git a/src/.backup/admin/itemVer.ts b/src/.backup/admin/itemVer.ts deleted file mode 100644 index 5ae65a4..0000000 --- a/src/.backup/admin/itemVer.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { prisma } from '../../../core/database/prisma'; -import type { TextBasedChannel } from 'discord.js'; -import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2'; - -export const command: CommandMessage = { - name: 'item-ver', - type: 'message', - aliases: ['ver-item', 'item-view'], - cooldown: 3, - description: 'Ver detalles de un item específico', - usage: 'item-ver ', - run: async (message, args, client: Amayo) => { - const guildId = message.guild!.id; - const key = args[0]?.trim(); - - if (!key) { - await message.reply('Uso: `!item-ver `\nEjemplo: `!item-ver tool.pickaxe.iron`'); - return; - } - - const item = await prisma.economyItem.findFirst({ - where: { - key, - OR: [{ guildId }, { guildId: null }] - } - }); - - if (!item) { - await message.reply(`❌ No se encontró el item con key \`${key}\``); - return; - } - - const props = item.props as any || {}; - const tags = item.tags || []; - - const blocks = [ - textBlock(`# 🛠️ ${item.name || item.key}`), - dividerBlock(), - textBlock([ - `**Key:** \`${item.key}\``, - `**Nombre:** ${item.name || '*Sin nombre*'}`, - `**Descripción:** ${item.description || '*Sin descripción*'}`, - `**Categoría:** ${item.category || '*Sin categoría*'}`, - `**Stackable:** ${item.stackable ? 'Sí' : 'No'}`, - `**Máx. Inventario:** ${item.maxPerInventory ?? 'Ilimitado'}`, - `**Ámbito:** ${item.guildId ? '📍 Local del servidor' : '🌐 Global'}`, - ].join('\n')), - ]; - - if (tags.length > 0) { - blocks.push(dividerBlock()); - blocks.push(textBlock(`**Tags:** ${tags.join(', ')}`)); - } - - if (item.icon) { - blocks.push(dividerBlock()); - blocks.push(textBlock(`**Icon URL:** ${item.icon}`)); - } - - if (Object.keys(props).length > 0) { - blocks.push(dividerBlock()); - blocks.push(textBlock(`**Props (JSON):**\n\`\`\`json\n${JSON.stringify(props, null, 2)}\n\`\`\``)); - } - - const display = buildDisplay(0x00D9FF, blocks); - - const channel = message.channel as TextBasedChannel & { send: Function }; - await (channel.send as any)({ - content: null, - flags: 32768, - components: [display], - reply: { messageReference: message.id } - }); - } -}; diff --git a/src/.backup/admin/itemsLista.ts b/src/.backup/admin/itemsLista.ts deleted file mode 100644 index a57e488..0000000 --- a/src/.backup/admin/itemsLista.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { prisma } from "../../../core/database/prisma"; -import { ComponentType, ButtonStyle } from "discord-api-types/v10"; -import type { MessageComponentInteraction, TextBasedChannel } from "discord.js"; -import { - buildDisplay, - dividerBlock, - textBlock, -} from "../../../core/lib/componentsV2"; - -export const command: CommandMessage = { - name: "items-lista", - type: "message", - aliases: ["lista-items", "items-list"], - cooldown: 5, - description: "Ver lista de todos los items del servidor", - usage: "items-lista [pagina]", - run: async (message, args, client: Amayo) => { - const guildId = message.guild!.id; - const perPage = 8; - - async function buildPayload(page: number) { - const total = await prisma.economyItem.count({ - where: { OR: [{ guildId }, { guildId: null }] }, - }); - const items = await prisma.economyItem.findMany({ - where: { OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ category: "asc" }, { name: "asc" }], - skip: (page - 1) * perPage, - take: perPage, - }); - - const totalPages = Math.max(1, Math.ceil(total / perPage)); - - const displayBlocks = [ - textBlock(`# 🛠️ Lista de Items`), - dividerBlock(), - textBlock(`Página ${page}/${totalPages} • Total: ${total}`), - dividerBlock({ divider: false, spacing: 2 }), - ...items.flatMap((item, index) => { - const lines = [ - `**${item.name || item.key}**`, - `└ Key: \`${item.key}\``, - `└ Categoría: ${item.category || "*Sin categoría*"}`, - `└ ${item.stackable ? "📚 Apilable" : "🔒 No apilable"}${ - item.maxPerInventory ? ` (Máx: ${item.maxPerInventory})` : "" - }${item.guildId === guildId ? " • 📍 Local" : " • 🌐 Global"}`, - ].join("\n"); - - const blocks = [textBlock(lines)]; - if (index < items.length - 1) { - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - } - return blocks; - }), - ]; - - const display = buildDisplay(0x00d9ff, displayBlocks); - - const buttons: any[] = []; - if (page > 1) { - buttons.push({ - type: ComponentType.Button, - style: ButtonStyle.Secondary, - label: "◀ Anterior", - custom_id: `items_prev_${page}`, - }); - } - if (page < totalPages) { - buttons.push({ - type: ComponentType.Button, - style: ButtonStyle.Secondary, - label: "Siguiente ▶", - custom_id: `items_next_${page}`, - }); - } - buttons.push({ - type: ComponentType.Button, - style: ButtonStyle.Primary, - label: "Ver Detalle", - custom_id: "items_detail", - }); - - const components: any[] = [display]; - if (buttons.length > 0) - components.push({ type: ComponentType.ActionRow, components: buttons }); - return { components, total, totalPages }; - } - - const page = parseInt(args[0]) || 1; - const channel = message.channel as TextBasedChannel & { send: Function }; - const initial = await buildPayload(page); - if (initial.total === 0) { - await message.reply("No hay items configurados en este servidor."); - return; - } - - const msg = await (channel.send as any)({ - content: null, - flags: 32768, - reply: { messageReference: message.id }, - components: initial.components, - }); - - let collector = msg.createMessageComponentCollector({ - time: 5 * 60_000, - filter: (i) => i.user.id === message.author.id, - }); - - collector.on("collect", async (i: MessageComponentInteraction) => { - if (!i.isButton()) return; - - if ( - i.customId.startsWith("items_prev_") || - i.customId.startsWith("items_next_") - ) { - const parts = i.customId.split("_"); - const currentPage = parseInt(parts[2]); - const newPage = i.customId.startsWith("items_prev_") - ? currentPage - 1 - : currentPage + 1; - await i.deferUpdate(); - // rebuild payload and edit message - try { - const p = await buildPayload(newPage); - await msg.edit({ content: null, components: p.components }); - } catch (e) { - await i.followUp({ - content: `❌ Error actualizando página: ${(e as any).message || e}`, - flags: 64, - }); - } - return; - } - - if (i.customId === "items_detail") { - await i.reply({ - content: - "💡 Usa `!item-ver ` para ver detalles de un item específico.", - flags: 64, - }); - } - }); - }, -}; diff --git a/src/.backup/admin/logroCrear.ts b/src/.backup/admin/logroCrear.ts deleted file mode 100644 index 7536943..0000000 --- a/src/.backup/admin/logroCrear.ts +++ /dev/null @@ -1,365 +0,0 @@ -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 {ButtonStyle, ComponentType, TextInputStyle} from 'discord-api-types/v10'; -import {buildDisplay, dividerBlock, textBlock} from '../../../core/lib/componentsV2'; -import type {ButtonInteraction, MessageComponentInteraction, TextBasedChannel} from 'discord.js'; - -interface AchievementState { - key: string; - name?: string; - description?: string; - category?: string; - icon?: string; - requirements?: any; - rewards?: any; - points?: number; - hidden?: boolean; -} - -export const command: CommandMessage = { - name: 'logro-crear', - type: 'message', - aliases: ['crear-logro', 'achievement-create'], - cooldown: 10, - description: 'Crea un logro para el servidor con editor interactivo', - usage: 'logro-crear ', - 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 key = args[0]?.trim(); - if (!key) { - await message.reply('Uso: `!logro-crear `\nEjemplo: `!logro-crear master_fisher`'); - return; - } - - const guildId = message.guild!.id; - const exists = await prisma.achievement.findFirst({ where: { key, guildId } }); - if (exists) { - await message.reply('❌ Ya existe un logro con esa key en este servidor.'); - return; - } - - const state: AchievementState = { - key, - category: 'economy', - points: 10, - hidden: false, - requirements: { type: 'mine_count', value: 1 }, - rewards: { coins: 100 } - }; - - const channel = message.channel as TextBasedChannel & { send: Function }; - const editorMsg = await (channel.send as any)({ - content: null, - flags: 32768, - reply: { messageReference: message.id }, - components: buildEditorComponents(state) - }); - - const collector = editorMsg.createMessageComponentCollector({ - time: 30 * 60_000, - filter: (i) => i.user.id === message.author.id - }); - - collector.on('collect', async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - - switch (i.customId) { - case 'ach_cancel': - await i.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - buildDisplay(0xFF0000, [ - textBlock('**❌ Creación de logro cancelada.**') - ]) - ] - }); - collector.stop('cancel'); - return; - - case 'ach_base': - await showBaseModal(i as ButtonInteraction, state, editorMsg); - return; - - case 'ach_req': - await showRequirementsModal(i as ButtonInteraction, state, editorMsg); - return; - - case 'ach_reward': - await showRewardsModal(i as ButtonInteraction, state, editorMsg); - return; - - case 'ach_save': - if (!state.name || !state.description) { - await i.reply({ content: '❌ Completa al menos el nombre y descripción.', flags: 64 }); - return; - } - - await prisma.achievement.create({ - data: { - guildId, - key: state.key, - name: state.name!, - description: state.description!, - category: state.category || 'economy', - icon: state.icon, - requirements: state.requirements as any || {}, - rewards: state.rewards as any || {}, - points: state.points || 10, - hidden: state.hidden || false - } - }); - - await i.reply({ content: '✅ Logro creado exitosamente!', flags: 64 }); - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - buildDisplay(0x00FF00, [ - textBlock(`**✅ Logro \`${state.key}\` creado exitosamente.**`) - ]) - ] - }); - collector.stop('saved'); - return; - } - } catch (e: any) { - console.error('Error en editor de logros:', e); - if (!i.deferred && !i.replied) { - await i.reply({ content: '❌ Error procesando la acción.', flags: 64 }); - } - } - }); - - collector.on('end', async (_c, r) => { - if (r === 'time') { - try { - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - buildDisplay(0xFFA500, [ - textBlock('**⏰ Editor expirado.**') - ]) - ] - }); - } catch {} - } - }); - } -}; - -function buildEditorDisplay(state: AchievementState) { - const baseInfo = [ - `**Nombre:** ${state.name || '*Sin definir*'}`, - `**Descripción:** ${state.description || '*Sin definir*'}`, - `**Categoría:** ${state.category || 'economy'}`, - `**Icono:** ${state.icon || '🏆'}`, - `**Puntos:** ${state.points ?? 10}`, - `**Oculto:** ${state.hidden ? 'Sí' : 'No'}`, - ].join('\n'); - - return buildDisplay(0xFFD700, [ - textBlock(`# 🏆 Creando Logro: \`${state.key}\``), - dividerBlock(), - textBlock(baseInfo), - dividerBlock(), - textBlock(`**Requisitos:**\n\`\`\`json\n${JSON.stringify(state.requirements, null, 2)}\n\`\`\``), - dividerBlock(), - textBlock(`**Recompensas:**\n\`\`\`json\n${JSON.stringify(state.rewards, null, 2)}\n\`\`\``), - ]); -} - -function buildEditorComponents(state: AchievementState) { - return [ - buildEditorDisplay(state), - { - type: ComponentType.ActionRow, - components: [ - { type: ComponentType.Button, style: ButtonStyle.Primary, label: 'Base', custom_id: 'ach_base' }, - { type: ComponentType.Button, style: ButtonStyle.Secondary, label: 'Requisitos', custom_id: 'ach_req' }, - { type: ComponentType.Button, style: ButtonStyle.Secondary, label: 'Recompensas', custom_id: 'ach_reward' }, - { type: ComponentType.Button, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'ach_save' }, - { type: ComponentType.Button, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'ach_cancel' } - ] - } - ]; -} - -async function showBaseModal(i: ButtonInteraction, state: AchievementState, editorMsg: any) { - const modal = { - title: 'Información Base del Logro', - customId: 'ach_base_modal', - components: [ - { - type: ComponentType.Label, - label: 'Nombre del logro', - component: { - type: ComponentType.TextInput, - customId: 'name', - style: TextInputStyle.Short, - required: true, - value: state.name || '', - placeholder: 'Ej: Maestro Pescador' - } - }, - { - type: ComponentType.Label, - label: 'Descripción', - component: { - type: ComponentType.TextInput, - customId: 'description', - style: TextInputStyle.Paragraph, - required: true, - value: state.description || '', - placeholder: 'Ej: Pesca 100 veces' - } - }, - { - type: ComponentType.Label, - label: 'Categoría', - component: { - type: ComponentType.TextInput, - customId: 'category', - style: TextInputStyle.Short, - required: false, - placeholder: '(mining/fishing/combat/economy/crafting)', - value: state.category || 'economy' - } - }, - { - type: ComponentType.Label, - label: 'Icono (emoji)', - component: { - type: ComponentType.TextInput, - customId: 'icon', - style: TextInputStyle.Short, - required: false, - value: state.icon || '🏆' - } - }, - { - type: ComponentType.Label, - label: 'Puntos (número)', - component: { - type: ComponentType.TextInput, - customId: 'points', - style: TextInputStyle.Short, - required: false, - value: String(state.points || 10) - } - } - ] - } as const; - - await i.showModal(modal); - - const submit = await i.awaitModalSubmit({ time: 5 * 60_000 }).catch(() => null); - if (!submit) return; - - state.name = submit.components.getTextInputValue('name'); - state.description = submit.components.getTextInputValue('description'); - state.category = submit.components.getTextInputValue('category') || 'economy'; - state.icon = submit.components.getTextInputValue('icon') || '🏆'; - state.points = parseInt(submit.components.getTextInputValue('points')) || 10; - - await submit.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildEditorComponents(state) - }); -} - -async function showRequirementsModal(i: ButtonInteraction, state: AchievementState, editorMsg: any) { - const modal = { - title: 'Requisitos del Logro', - customId: 'ach_req_modal', - components: [ - { - type: ComponentType.TextDisplay, - content: 'Formato JSON con "type" y "value"' - }, - { - type: ComponentType.Label, - label: 'Requisitos (JSON)', - component: { - type: ComponentType.TextInput, - customId: 'requirements', - style: TextInputStyle.Paragraph, - required: true, - value: JSON.stringify(state.requirements, null, 2), - placeholder: '{"type": "mine_count", "value": 100}' - } - } - ] - } as const; - - await i.showModal(modal); - - const submit = await i.awaitModalSubmit({ time: 5 * 60_000 }).catch(() => null); - if (!submit) return; - - try { - state.requirements = JSON.parse(submit.components.getTextInputValue('requirements')); - await submit.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildEditorComponents(state) - }); - } catch (e) { - await submit.reply({ content: '❌ JSON inválido en requisitos.', flags: 64 }); - } -} - -async function showRewardsModal(i: ButtonInteraction, state: AchievementState, editorMsg: any) { - const modal = { - title: 'Recompensas del Logro', - customId: 'ach_reward_modal', - components: [ - { - type: ComponentType.TextDisplay, - content: 'Formato JSON con coins, items, etc.' - }, - { - type: ComponentType.Label, - label: 'Recompensas (JSON)', - component: { - type: ComponentType.TextInput, - customId: 'rewards', - style: TextInputStyle.Paragraph, - required: true, - value: JSON.stringify(state.rewards, null, 2), - placeholder: '{"coins": 1000, "items": [{"key": "item.key", "quantity": 1}]}' - } - } - ] - } as const; - - await i.showModal(modal); - - const submit = await i.awaitModalSubmit({ time: 5 * 60_000 }).catch(() => null); - if (!submit) return; - - try { - state.rewards = JSON.parse(submit.components.getTextInputValue('rewards')); - await submit.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildEditorComponents(state) - }); - } catch (e) { - await submit.reply({ content: '❌ JSON inválido en recompensas.', flags: 64 }); - } -} diff --git a/src/.backup/admin/logroEliminar.ts b/src/.backup/admin/logroEliminar.ts deleted file mode 100644 index 01b16c0..0000000 --- a/src/.backup/admin/logroEliminar.ts +++ /dev/null @@ -1,61 +0,0 @@ -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'; - -export const command: CommandMessage = { - name: 'logro-eliminar', - type: 'message', - aliases: ['eliminar-logro', 'achievement-delete'], - cooldown: 5, - description: 'Eliminar un logro del servidor', - usage: 'logro-eliminar ', - 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 guildId = message.guild!.id; - const key = args[0]?.trim(); - - if (!key) { - await message.reply('Uso: `!logro-eliminar `\nEjemplo: `!logro-eliminar test_achievement`'); - return; - } - - const achievement = await prisma.achievement.findFirst({ - where: { key, guildId } - }); - - if (!achievement) { - await message.reply(`❌ No se encontró el logro local con key \`${key}\` en este servidor.\n` + - `💡 Solo puedes eliminar logros locales del servidor, no globales.`); - return; - } - - // Contar cuántos jugadores lo han desbloqueado - const unlockedCount = await prisma.playerAchievement.count({ - where: { - achievementId: achievement.id, - unlockedAt: { not: null } - } - }); - - // Eliminar progreso de jugadores primero - await prisma.playerAchievement.deleteMany({ - where: { achievementId: achievement.id } - }); - - // Eliminar el logro - await prisma.achievement.delete({ - where: { id: achievement.id } - }); - - await message.reply( - `✅ Logro \`${key}\` eliminado exitosamente.\n` + - `${unlockedCount > 0 ? `⚠️ Se eliminó el progreso de ${unlockedCount} jugador(es).` : ''}` - ); - } -}; diff --git a/src/.backup/admin/logroVer.ts b/src/.backup/admin/logroVer.ts deleted file mode 100644 index bbd860c..0000000 --- a/src/.backup/admin/logroVer.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { prisma } from '../../../core/database/prisma'; -import type { TextBasedChannel } from 'discord.js'; -import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2'; - -export const command: CommandMessage = { - name: 'logro-ver', - type: 'message', - aliases: ['ver-logro', 'achievement-view'], - cooldown: 3, - description: 'Ver detalles de un logro específico', - usage: 'logro-ver ', - run: async (message, args, client: Amayo) => { - const guildId = message.guild!.id; - const key = args[0]?.trim(); - - if (!key) { - await message.reply('Uso: `!logro-ver `\nEjemplo: `!logro-ver first_mine`'); - return; - } - - const achievement = await prisma.achievement.findFirst({ - where: { - key, - OR: [{ guildId }, { guildId: null }] - }, - include: { - unlocked: { - take: 10, - orderBy: { unlockedAt: 'desc' }, - where: { unlockedAt: { not: null } } - } - } - }); - - if (!achievement) { - await message.reply(`❌ No se encontró el logro con key \`${key}\``); - return; - } - - const unlockedCount = await prisma.playerAchievement.count({ - where: { - achievementId: achievement.id, - guildId, - unlockedAt: { not: null } - } - }); - - const req = achievement.requirements as any; - const rew = achievement.rewards as any; - - const blocks = [ - textBlock(`${achievement.icon || '🏆'} **${achievement.name}**`), - dividerBlock(), - textBlock([ - `**Descripción:** ${achievement.description}`, - `**Key:** \`${achievement.key}\``, - `**Categoría:** ${achievement.category}`, - `**Puntos:** ${achievement.points} pts`, - `**Visibilidad:** ${achievement.hidden ? '🔒 Oculto' : '👁️ Visible'}`, - `**Ámbito:** ${achievement.guildId ? '📍 Local del servidor' : '🌐 Global'}`, - `**Desbloqueados:** ${unlockedCount} jugadores`, - ].join('\n')), - dividerBlock(), - textBlock(`**📋 Requisitos:**\n\`\`\`json\n${JSON.stringify(req, null, 2)}\n\`\`\``), - dividerBlock(), - textBlock(`**🎁 Recompensas:**\n\`\`\`json\n${JSON.stringify(rew, null, 2)}\n\`\`\``), - ]; - - if (achievement.unlocked.length > 0) { - const unlockedLines = achievement.unlocked.slice(0, 5) - .map(pa => `• <@${pa.userId}> - ${pa.unlockedAt ? new Date(pa.unlockedAt).toLocaleDateString() : 'N/A'}`) - .join('\n'); - blocks.push(dividerBlock()); - blocks.push(textBlock(`**🏆 Últimos Desbloqueados:**\n${unlockedLines}`)); - } - - const display = buildDisplay(0xFFD700, blocks); - - const channel = message.channel as TextBasedChannel & { send: Function }; - await (channel.send as any)({ - content: null, - flags: 32768, - components: [display], - reply: { messageReference: message.id } - }); - } -}; diff --git a/src/.backup/admin/logrosLista.ts b/src/.backup/admin/logrosLista.ts deleted file mode 100644 index a77f23b..0000000 --- a/src/.backup/admin/logrosLista.ts +++ /dev/null @@ -1,134 +0,0 @@ -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 { ComponentType, ButtonStyle } from 'discord-api-types/v10'; -import type { MessageComponentInteraction, TextBasedChannel } from 'discord.js'; -import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2'; - -export const command: CommandMessage = { - name: 'logros-lista', - type: 'message', - aliases: ['lista-logros', 'achievements-list'], - cooldown: 5, - description: 'Ver lista de todos los logros del servidor', - usage: 'logros-lista [pagina]', - run: async (message, args, client: Amayo) => { - const guildId = message.guild!.id; - const page = parseInt(args[0]) || 1; - const perPage = 5; - - const total = await prisma.achievement.count({ - where: { OR: [{ guildId }, { guildId: null }] } - }); - - const achievements = await prisma.achievement.findMany({ - where: { OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ category: 'asc' }, { points: 'desc' }], - skip: (page - 1) * perPage, - take: perPage - }); - - if (achievements.length === 0) { - await message.reply('No hay logros configurados en este servidor.'); - return; - } - - const totalPages = Math.ceil(total / perPage); - - const displayBlocks = [ - textBlock(`# 🏆 Lista de Logros`), - dividerBlock(), - textBlock(`Página ${page}/${totalPages} • Total: ${total}`), - dividerBlock({ divider: false, spacing: 2 }), - ...achievements.flatMap((ach, index) => { - const lines = [ - `${ach.icon || '🏆'} **${ach.name}** (${ach.points} pts)`, - `└ Key: \`${ach.key}\``, - `└ Categoría: ${ach.category}`, - `└ ${ach.description}`, - `└ ${ach.hidden ? '🔒 Oculto' : '👁️ Visible'}${ach.guildId === guildId ? ' • 📍 Local' : ' • 🌐 Global'}`, - ].join('\n'); - - const blocks = [textBlock(lines)]; - if (index < achievements.length - 1) { - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - } - return blocks; - }) - ]; - - const display = buildDisplay(0xFFD700, displayBlocks); - - const buttons: any[] = []; - - if (page > 1) { - buttons.push({ - type: ComponentType.Button, - style: ButtonStyle.Secondary, - label: '◀ Anterior', - custom_id: `ach_list_prev_${page}` - }); - } - - if (page < totalPages) { - buttons.push({ - type: ComponentType.Button, - style: ButtonStyle.Secondary, - label: 'Siguiente ▶', - custom_id: `ach_list_next_${page}` - }); - } - - buttons.push({ - type: ComponentType.Button, - style: ButtonStyle.Primary, - label: 'Ver Detalle', - custom_id: 'ach_view_detail' - }); - - const channel = message.channel as TextBasedChannel & { send: Function }; - const msg = await (channel.send as any)({ - content: null, - flags: 32768, - reply: { messageReference: message.id }, - components: [ - display, - ...(buttons.length > 0 ? [{ - type: ComponentType.ActionRow, - components: buttons - }] : []) - ] - }); - - const collector = msg.createMessageComponentCollector({ - time: 5 * 60_000, - filter: (i) => i.user.id === message.author.id - }); - - collector.on('collect', async (i: MessageComponentInteraction) => { - if (!i.isButton()) return; - - if (i.customId.startsWith('ach_list_prev_')) { - const currentPage = parseInt(i.customId.split('_')[3]); - await i.deferUpdate(); - // Re-ejecutar comando con página anterior - args[0] = String(currentPage - 1); - await command.run!(message, args, client); - collector.stop(); - } else if (i.customId.startsWith('ach_list_next_')) { - const currentPage = parseInt(i.customId.split('_')[3]); - await i.deferUpdate(); - // Re-ejecutar comando con página siguiente - args[0] = String(currentPage + 1); - await command.run!(message, args, client); - collector.stop(); - } else if (i.customId === 'ach_view_detail') { - await i.reply({ - content: '💡 Usa `!logro-ver ` para ver detalles de un logro específico.', - flags: 64 - }); - } - }); - } -}; diff --git a/src/.backup/admin/misionCrear.ts b/src/.backup/admin/misionCrear.ts deleted file mode 100644 index c2643ce..0000000 --- a/src/.backup/admin/misionCrear.ts +++ /dev/null @@ -1,373 +0,0 @@ -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 { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10'; -import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2'; -import type { ButtonInteraction, MessageComponentInteraction, TextBasedChannel } from 'discord.js'; - -interface QuestState { - key: string; - name?: string; - description?: string; - category?: string; - type?: string; - icon?: string; - requirements?: any; - rewards?: any; - repeatable?: boolean; -} - -export const command: CommandMessage = { - name: 'mision-crear', - type: 'message', - aliases: ['crear-mision', 'quest-create'], - cooldown: 10, - description: 'Crea una misión para el servidor con editor interactivo', - usage: 'mision-crear ', - 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 key = args[0]?.trim(); - if (!key) { - await message.reply('Uso: `!mision-crear `\nEjemplo: `!mision-crear daily_mine_10`'); - return; - } - - const guildId = message.guild!.id; - const exists = await prisma.quest.findFirst({ where: { key, guildId } }); - if (exists) { - await message.reply('❌ Ya existe una misión con esa key en este servidor.'); - return; - } - - const state: QuestState = { - key, - category: 'mining', - type: 'daily', - repeatable: false, - requirements: { type: 'mine_count', count: 10 }, - rewards: { coins: 500 } - }; - - const channel = message.channel as TextBasedChannel & { send: Function }; - const editorMsg = await (channel.send as any)({ - content: null, - flags: 32768, - reply: { messageReference: message.id }, - components: buildEditorComponents(state) - }); - - const collector = editorMsg.createMessageComponentCollector({ - time: 30 * 60_000, - filter: (i) => i.user.id === message.author.id - }); - - collector.on('collect', async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - - switch (i.customId) { - case 'quest_cancel': - await i.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - buildDisplay(0xFF0000, [ - textBlock('**❌ Creación de misión cancelada.**') - ]) - ] - }); - collector.stop('cancel'); - return; - - case 'quest_base': - await showBaseModal(i as ButtonInteraction, state, editorMsg); - return; - - case 'quest_req': - await showRequirementsModal(i as ButtonInteraction, state, editorMsg); - return; - - case 'quest_reward': - await showRewardsModal(i as ButtonInteraction, state, editorMsg); - return; - - case 'quest_save': - if (!state.name || !state.description) { - await i.reply({ content: '❌ Completa al menos el nombre y descripción.', flags: 64 }); - return; - } - - await prisma.quest.create({ - data: { - guildId, - key: state.key, - name: state.name!, - description: state.description!, - category: state.category || 'mining', - type: state.type || 'daily', - icon: state.icon, - requirements: state.requirements as any || {}, - rewards: state.rewards as any || {}, - repeatable: state.repeatable || false, - active: true - } - }); - - await i.reply({ content: '✅ Misión creada exitosamente.', flags: 64 }); - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - buildDisplay(0x00FF00, [ - textBlock(`**✅ Misión \`${state.key}\` creada exitosamente.**`) - ]) - ] - }); - collector.stop('saved'); - return; - } - } catch (e: any) { - console.error('Error en editor de misiones:', e); - if (!i.deferred && !i.replied) { - await i.reply({ content: '❌ Error procesando la acción.', flags: 64 }); - } - } - }); - - collector.on('end', async (_c, r) => { - if (r === 'time') { - try { - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - buildDisplay(0xFFA500, [ - textBlock('**⏰ Editor expirado.**') - ]) - ] - }); - } catch {} - } - }); - } -}; - -function buildEditorDisplay(state: QuestState) { - const typeEmojis: Record = { - daily: '📅', - weekly: '📆', - permanent: '♾️', - event: '🎉' - }; - - const baseInfo = [ - `**Nombre:** ${state.name || '*Sin definir*'}`, - `**Descripción:** ${state.description || '*Sin definir*'}`, - `**Categoría:** ${state.category || 'mining'}`, - `**Tipo:** ${typeEmojis[state.type || 'daily']} ${state.type || 'daily'}`, - `**Icono:** ${state.icon || '📋'}`, - `**Repetible:** ${state.repeatable ? 'Sí' : 'No'}` - ].join('\n'); - - return buildDisplay(0x5865F2, [ - textBlock(`# 📜 Creando Misión: \`${state.key}\``), - dividerBlock(), - textBlock(baseInfo), - dividerBlock(), - textBlock(`**Requisitos:**\n\`\`\`json\n${JSON.stringify(state.requirements, null, 2)}\n\`\`\``), - dividerBlock(), - textBlock(`**Recompensas:**\n\`\`\`json\n${JSON.stringify(state.rewards, null, 2)}\n\`\`\``) - ]); -} - -function buildEditorComponents(state: QuestState) { - return [ - buildEditorDisplay(state), - { - type: ComponentType.ActionRow, - components: [ - { type: ComponentType.Button, style: ButtonStyle.Primary, label: 'Base', custom_id: 'quest_base' }, - { type: ComponentType.Button, style: ButtonStyle.Secondary, label: 'Requisitos', custom_id: 'quest_req' }, - { type: ComponentType.Button, style: ButtonStyle.Secondary, label: 'Recompensas', custom_id: 'quest_reward' }, - { type: ComponentType.Button, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'quest_save' }, - { type: ComponentType.Button, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'quest_cancel' } - ] - } - ]; -} - -async function showBaseModal(i: ButtonInteraction, state: QuestState, editorMsg: any) { - const modal = { - title: 'Información Base de la Misión', - customId: 'quest_base_modal', - components: [ - { - type: ComponentType.Label, - label: 'Nombre de la misión', - component: { - type: ComponentType.TextInput, - customId: 'name', - style: TextInputStyle.Short, - required: true, - value: state.name || '', - placeholder: 'Ej: Minero Diario' - } - }, - { - type: ComponentType.Label, - label: 'Descripción', - component: { - type: ComponentType.TextInput, - customId: 'description', - style: TextInputStyle.Paragraph, - required: true, - value: state.description || '', - placeholder: 'Ej: Mina 10 veces hoy' - } - }, - { - type: ComponentType.Label, - label: 'Categoría', - component: { - type: ComponentType.TextInput, - customId: 'category', - style: TextInputStyle.Short, - required: false, - placeholder: "(mining/fishing/combat/economy/crafting)", - value: state.category || 'mining' - } - }, - { - type: ComponentType.Label, - label: 'Tipo (daily/weekly/permanent/event)', - component: { - type: ComponentType.TextInput, - customId: 'type', - style: TextInputStyle.Short, - required: false, - value: state.type || 'daily' - } - }, - { - type: ComponentType.Label, - label: 'Icono (emoji)', - component: { - type: ComponentType.TextInput, - customId: 'icon', - style: TextInputStyle.Short, - required: false, - value: state.icon || '📋' - } - } - ] - } as const; - - await i.showModal(modal); - - const submit = await i.awaitModalSubmit({ time: 5 * 60_000 }).catch(() => null); - if (!submit) return; - - state.name = submit.components.getTextInputValue('name'); - state.description = submit.components.getTextInputValue('description'); - state.category = submit.components.getTextInputValue('category') || 'mining'; - state.type = submit.components.getTextInputValue('type') || 'daily'; - state.icon = submit.components.getTextInputValue('icon') || '📋'; - - await submit.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildEditorComponents(state) - }); -} - -async function showRequirementsModal(i: ButtonInteraction, state: QuestState, editorMsg: any) { - const modal = { - title: 'Requisitos de la Misión', - customId: 'quest_req_modal', - components: [ - { - type: ComponentType.TextDisplay, - content: 'Formato JSON con "type" y "count"' - }, - { - type: ComponentType.Label, - label: 'Requisitos (JSON)', - component: { - type: ComponentType.TextInput, - customId: 'requirements', - style: TextInputStyle.Paragraph, - required: true, - value: JSON.stringify(state.requirements, null, 2), - placeholder: '{"type": "mine_count", "count": 10}' - } - } - ] - } as const; - - await i.showModal(modal); - - const submit = await i.awaitModalSubmit({ time: 5 * 60_000 }).catch(() => null); - if (!submit) return; - - try { - state.requirements = JSON.parse(submit.components.getTextInputValue('requirements')); - await submit.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildEditorComponents(state) - }); - } catch (e) { - await submit.reply({ content: '❌ JSON inválido en requisitos.', flags: 64 }); - } -} - -async function showRewardsModal(i: ButtonInteraction, state: QuestState, editorMsg: any) { - const modal = { - title: 'Recompensas de la Misión', - customId: 'quest_reward_modal', - components: [ - { - type: ComponentType.TextDisplay, - content: 'Formato JSON con coins, items, xp, etc.' - }, - { - type: ComponentType.Label, - label: 'Recompensas (JSON)', - component: { - type: ComponentType.TextInput, - customId: 'rewards', - style: TextInputStyle.Paragraph, - required: true, - value: JSON.stringify(state.rewards, null, 2), - placeholder: '{"coins": 500, "items": [{"key": "item.key", "quantity": 1}]}' - } - } - ] - } as const; - - await i.showModal(modal); - - const submit = await i.awaitModalSubmit({ time: 5 * 60_000 }).catch(() => null); - if (!submit) return; - - try { - state.rewards = JSON.parse(submit.components.getTextInputValue('rewards')); - await submit.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildEditorComponents(state) - }); - } catch (e) { - await submit.reply({ content: '❌ JSON inválido en recompensas.', flags: 64 }); - } -} diff --git a/src/.backup/admin/misionEliminar.ts b/src/.backup/admin/misionEliminar.ts deleted file mode 100644 index 168a25a..0000000 --- a/src/.backup/admin/misionEliminar.ts +++ /dev/null @@ -1,58 +0,0 @@ -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'; - -export const command: CommandMessage = { - name: 'mision-eliminar', - type: 'message', - aliases: ['eliminar-mision', 'quest-delete'], - cooldown: 5, - description: 'Eliminar una misión del servidor', - usage: 'mision-eliminar ', - 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 guildId = message.guild!.id; - const key = args[0]?.trim(); - - if (!key) { - await message.reply('Uso: `!mision-eliminar `\nEjemplo: `!mision-eliminar daily_mine`'); - return; - } - - const quest = await prisma.quest.findFirst({ - where: { key, guildId } - }); - - if (!quest) { - await message.reply(`❌ No se encontró la misión local con key \`${key}\` en este servidor.\n` + - `💡 Solo puedes eliminar misiones locales del servidor, no globales.`); - return; - } - - // Contar cuántos progresos existen - const progressCount = await prisma.questProgress.count({ - where: { questId: quest.id } - }); - - // Eliminar progreso de jugadores primero - await prisma.questProgress.deleteMany({ - where: { questId: quest.id } - }); - - // Eliminar la misión - await prisma.quest.delete({ - where: { id: quest.id } - }); - - await message.reply( - `✅ Misión \`${key}\` eliminada exitosamente.\n` + - `${progressCount > 0 ? `⚠️ Se eliminó el progreso de ${progressCount} registro(s).` : ''}` - ); - } -}; diff --git a/src/.backup/admin/misionVer.ts b/src/.backup/admin/misionVer.ts deleted file mode 100644 index 89c81cf..0000000 --- a/src/.backup/admin/misionVer.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { prisma } from '../../../core/database/prisma'; -import type { TextBasedChannel } from 'discord.js'; - -export const command: CommandMessage = { - name: 'mision-ver', - type: 'message', - aliases: ['ver-mision', 'quest-view'], - cooldown: 3, - description: 'Ver detalles de una misión específica', - usage: 'mision-ver ', - run: async (message, args, client: Amayo) => { - const guildId = message.guild!.id; - const key = args[0]?.trim(); - - if (!key) { - await message.reply('Uso: `!mision-ver `\nEjemplo: `!mision-ver daily_mine`'); - return; - } - - const quest = await prisma.quest.findFirst({ - where: { - key, - OR: [{ guildId }, { guildId: null }] - }, - include: { - progress: { - take: 10, - orderBy: { completedAt: 'desc' }, - where: { completed: true } - } - } - }); - - if (!quest) { - await message.reply(`❌ No se encontró la misión con key \`${key}\``); - return; - } - - const completedCount = await prisma.questProgress.count({ - where: { - questId: quest.id, - guildId, - completed: true - } - }); - - const claimedCount = await prisma.questProgress.count({ - where: { - questId: quest.id, - guildId, - claimed: true - } - }); - - const req = quest.requirements as any; - const rew = quest.rewards as any; - - const typeEmojis: Record = { - daily: '📅 Diaria', - weekly: '📆 Semanal', - permanent: '♾️ Permanente', - event: '🎉 Evento' - }; - - const display = { - type: 17, - accent_color: 0x5865F2, - components: [ - { - type: 9, - components: [ - { - type: 10, - content: `${quest.icon || '📋'} **${quest.name}**` - } - ] - }, - { type: 14, divider: true }, - { - type: 9, - components: [ - { - type: 10, - content: `**Descripción:** ${quest.description}\n` + - `**Key:** \`${quest.key}\`\n` + - `**Tipo:** ${typeEmojis[quest.type] || quest.type}\n` + - `**Categoría:** ${quest.category}\n` + - `**Estado:** ${quest.active ? '✅ Activa' : '❌ Inactiva'}\n` + - `**Repetible:** ${quest.repeatable ? '🔄 Sí' : '1️⃣ No'}\n` + - `**Ámbito:** ${quest.guildId ? '📍 Local del servidor' : '🌐 Global'}\n` + - `**Completadas:** ${completedCount} veces\n` + - `**Recompensas reclamadas:** ${claimedCount} veces` - } - ] - } - ] - }; - - if (quest.startAt || quest.endAt) { - display.components.push({ type: 14, divider: true }); - display.components.push({ - type: 9, - components: [ - { - type: 10, - content: `**⏰ Disponibilidad:**\n` + - (quest.startAt ? `Inicio: ${new Date(quest.startAt).toLocaleString()}\n` : '') + - (quest.endAt ? `Fin: ${new Date(quest.endAt).toLocaleString()}` : 'Sin fecha de fin') - } - ] - }); - } - - display.components.push({ type: 14, divider: true }); - display.components.push({ - type: 9, - components: [ - { - type: 10, - content: `**📋 Requisitos:**\n\`\`\`json\n${JSON.stringify(req, null, 2)}\n\`\`\`` - } - ] - }); - - display.components.push({ type: 14, divider: true }); - display.components.push({ - type: 9, - components: [ - { - type: 10, - content: `**🎁 Recompensas:**\n\`\`\`json\n${JSON.stringify(rew, null, 2)}\n\`\`\`` - } - ] - }); - - if (quest.progress.length > 0) { - display.components.push({ type: 14, divider: true }); - display.components.push({ - type: 9, - components: [ - { - type: 10, - content: `**✅ Últimas Completaciones:**\n` + - quest.progress.slice(0, 5).map(qp => - `• <@${qp.userId}> - ${qp.completedAt ? new Date(qp.completedAt).toLocaleDateString() : 'N/A'} ${qp.claimed ? '🎁' : ''}` - ).join('\n') - } - ] - }); - } - - const channel = message.channel as TextBasedChannel & { send: Function }; await (channel.send as any)({ flags: 32768, components: [display] }); - } -}; diff --git a/src/.backup/admin/misionesLista.ts b/src/.backup/admin/misionesLista.ts deleted file mode 100644 index 5462aba..0000000 --- a/src/.backup/admin/misionesLista.ts +++ /dev/null @@ -1,143 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { prisma } from '../../../core/database/prisma'; -import { ComponentType, ButtonStyle } from 'discord-api-types/v10'; -import type { MessageComponentInteraction, TextBasedChannel } from 'discord.js'; - -export const command: CommandMessage = { - name: 'misiones-lista', - type: 'message', - aliases: ['lista-misiones', 'quests-list'], - cooldown: 5, - description: 'Ver lista de todas las misiones del servidor', - usage: 'misiones-lista [pagina]', - run: async (message, args, client: Amayo) => { - const guildId = message.guild!.id; - const page = parseInt(args[0]) || 1; - const perPage = 5; - - const total = await prisma.quest.count({ - where: { OR: [{ guildId }, { guildId: null }] } - }); - - const quests = await prisma.quest.findMany({ - where: { OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ type: 'asc' }, { category: 'asc' }], - skip: (page - 1) * perPage, - take: perPage - }); - - if (quests.length === 0) { - await message.reply('No hay misiones configuradas en este servidor.'); - return; - } - - const totalPages = Math.ceil(total / perPage); - - const typeEmojis: Record = { - daily: '📅', - weekly: '📆', - permanent: '♾️', - event: '🎉' - }; - - const display = { - type: 17, - accent_color: 0x5865F2, - components: [ - { - type: 9, - components: [ - { - type: 10, - content: `**📜 Lista de Misiones**\nPágina ${page}/${totalPages} • Total: ${total}` - } - ] - }, - { type: 14, divider: true }, - ...quests.map(quest => ({ - type: 9, - components: [ - { - type: 10, - content: `${quest.icon || '📋'} **${quest.name}** ${typeEmojis[quest.type] || '📋'}\n` + - `└ Key: \`${quest.key}\`\n` + - `└ Tipo: ${quest.type} • Categoría: ${quest.category}\n` + - `└ ${quest.description}\n` + - `└ ${quest.active ? '✅ Activa' : '❌ Inactiva'} • ` + - `${quest.repeatable ? '🔄 Repetible' : '1️⃣ Una vez'}` + - (quest.guildId === guildId ? ' • 📍 Local' : ' • 🌐 Global') - } - ] - })) - ] - }; - - const buttons: any[] = []; - - if (page > 1) { - buttons.push({ - type: ComponentType.Button, - style: ButtonStyle.Secondary, - label: '◀ Anterior', - custom_id: `quest_list_prev_${page}` - }); - } - - if (page < totalPages) { - buttons.push({ - type: ComponentType.Button, - style: ButtonStyle.Secondary, - label: 'Siguiente ▶', - custom_id: `quest_list_next_${page}` - }); - } - - buttons.push({ - type: ComponentType.Button, - style: ButtonStyle.Primary, - label: 'Ver Detalle', - custom_id: 'quest_view_detail' - }); - - const channel = message.channel as TextBasedChannel & { send: Function }; - const msg = await (channel.send as any)({ - flags: 32768, - components: [ - display, - ...(buttons.length > 0 ? [{ - type: ComponentType.ActionRow, - components: buttons - }] : []) - ] - }); - - const collector = msg.createMessageComponentCollector({ - time: 5 * 60_000, - filter: (i) => i.user.id === message.author.id - }); - - collector.on('collect', async (i: MessageComponentInteraction) => { - if (!i.isButton()) return; - - if (i.customId.startsWith('quest_list_prev_')) { - const currentPage = parseInt(i.customId.split('_')[3]); - await i.deferUpdate(); - args[0] = String(currentPage - 1); - await command.run!(message, args, client); - collector.stop(); - } else if (i.customId.startsWith('quest_list_next_')) { - const currentPage = parseInt(i.customId.split('_')[3]); - await i.deferUpdate(); - args[0] = String(currentPage + 1); - await command.run!(message, args, client); - collector.stop(); - } else if (i.customId === 'quest_view_detail') { - await i.reply({ - content: '💡 Usa `!mision-ver ` para ver detalles de una misión específica.', - flags: 64 - }); - } - }); - } -}; diff --git a/src/.backup/admin/mobEliminar.ts b/src/.backup/admin/mobEliminar.ts deleted file mode 100644 index 71e0af2..0000000 --- a/src/.backup/admin/mobEliminar.ts +++ /dev/null @@ -1,47 +0,0 @@ -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"; - -export const command: CommandMessage = { - name: "mob-eliminar", - type: "message", - aliases: ["eliminar-mob", "mob-delete"], - cooldown: 5, - description: "Eliminar un mob del servidor", - usage: "mob-eliminar ", - 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 guildId = message.guild!.id; - const key = args[0]?.trim(); - - if (!key) { - await message.reply( - "Uso: `!mob-eliminar `\nEjemplo: `!mob-eliminar mob.goblin`" - ); - return; - } - - // Use admin.deleteMob to centralize logic - const { deleteMob } = await import("../../../game/mobs/admin.js"); - const deleted = await deleteMob(key); - if (!deleted) { - await message.reply( - `❌ No se encontró el mob local con key ${key} en este servidor.` - ); - return; - } - await message.reply(`✅ Mob ${key} eliminado exitosamente.`); - }, -}; diff --git a/src/.backup/admin/mobsLista.ts b/src/.backup/admin/mobsLista.ts deleted file mode 100644 index 79f083b..0000000 --- a/src/.backup/admin/mobsLista.ts +++ /dev/null @@ -1,126 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { ComponentType, ButtonStyle } from "discord-api-types/v10"; -import type { MessageComponentInteraction, TextBasedChannel } from "discord.js"; - -export const command: CommandMessage = { - name: "mobs-lista", - type: "message", - aliases: ["lista-mobs", "mobs-list"], - cooldown: 5, - description: "Ver lista de todos los mobs del servidor", - usage: "mobs-lista [pagina]", - run: async (message, args, client: Amayo) => { - const guildId = message.guild!.id; - const page = parseInt(args[0]) || 1; - const perPage = 6; - - // Use admin list (including built-ins and DB rows) - const { listMobsWithRows } = await import("../../../game/mobs/admin.js"); - const all = await listMobsWithRows(); - if (!all || all.length === 0) { - await message.reply("No hay mobs configurados en este servidor."); - return; - } - - const total = all.length; - const totalPages = Math.ceil(total / perPage); - const pageItems = all.slice( - (page - 1) * perPage, - (page - 1) * perPage + perPage - ); - - const display = { - type: 17, - accent_color: 0xff0000, - components: [ - { - type: 9, - components: [ - { - type: 10, - content: `**👾 Lista de Mobs**\nPágina ${page}/${totalPages} • Total: ${total}`, - }, - ], - }, - { type: 14, divider: true }, - ...pageItems.map((entry) => { - const mob = entry.def; - const stats = (mob.base as any) || {}; - return { - type: 9, - components: [ - { - type: 10, - content: - `**${mob.name || mob.key}**\n` + - `└ Key: \`${mob.key}\`\n` + - `└ ATK: ${stats.attack || 0} | HP: ${stats.hp || 0}\n` + - `└ ${entry.guildId === guildId ? "📍 Local" : "🌐 Global"}`, - }, - ], - }; - }), - ], - }; - - const buttons: any[] = []; - - if (page > 1) { - buttons.push({ - type: ComponentType.Button, - style: ButtonStyle.Secondary, - label: "◀ Anterior", - custom_id: `mobs_prev_${page}`, - }); - } - - if (page < totalPages) { - buttons.push({ - type: ComponentType.Button, - style: ButtonStyle.Secondary, - label: "Siguiente ▶", - custom_id: `mobs_next_${page}`, - }); - } - - const channel = message.channel as TextBasedChannel & { send: Function }; - const msg = await (channel.send as any)({ - flags: 32768, - components: [ - display, - ...(buttons.length > 0 - ? [ - { - type: ComponentType.ActionRow, - components: buttons, - }, - ] - : []), - ], - }); - - const collector = msg.createMessageComponentCollector({ - time: 5 * 60_000, - filter: (i) => i.user.id === message.author.id, - }); - - collector.on("collect", async (i: MessageComponentInteraction) => { - if (!i.isButton()) return; - - if (i.customId.startsWith("mobs_prev_")) { - const currentPage = parseInt(i.customId.split("_")[2]); - await i.deferUpdate(); - args[0] = String(currentPage - 1); - await command.run!(message, args, client); - collector.stop(); - } else if (i.customId.startsWith("mobs_next_")) { - const currentPage = parseInt(i.customId.split("_")[2]); - await i.deferUpdate(); - args[0] = String(currentPage + 1); - await command.run!(message, args, client); - collector.stop(); - } - }); - }, -}; diff --git a/src/.backup/admin/resetInventory.ts b/src/.backup/admin/resetInventory.ts deleted file mode 100644 index 02578b9..0000000 --- a/src/.backup/admin/resetInventory.ts +++ /dev/null @@ -1,185 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { prisma } from "../../../core/database/prisma"; -import type { Prisma } from "@prisma/client"; - -type ItemProps = { - breakable?: { - enabled?: boolean; - maxDurability?: number; - durabilityPerUse?: number; - }; - [k: string]: unknown; -}; - -type InventoryState = { - instances?: Array<{ - durability?: number; - expiresAt?: string; - notes?: string; - mutations?: string[]; - }>; - notes?: string; - [k: string]: unknown; -}; - -export const command: CommandMessage = { - name: "reset-inventory", - type: "message", - aliases: ["resetinv", "fix-stackable"], - cooldown: 0, - category: "Admin", - description: - "Resetea el inventario de herramientas/armas de un usuario para migrar de stackable a non-stackable con durabilidad.", - usage: "reset-inventory [@user]", - run: async (message, args, _client: Amayo) => { - // Solo el owner del bot puede ejecutar esto - if (message.author.id !== process.env.OWNER_ID) { - await message.reply("❌ Solo el owner del bot puede usar este comando."); - return; - } - - const targetUser = message.mentions.users.first() ?? message.author; - const guildId = message.guild!.id; - const userId = targetUser.id; - - await message.reply( - `🔄 Iniciando reseteo de inventario para <@${userId}>...` - ); - - try { - // Paso 1: Obtener todos los items non-stackable (herramientas/armas/armaduras/capas) - const nonStackableItems = await prisma.economyItem.findMany({ - where: { - stackable: false, - OR: [ - { key: { startsWith: "tool." } }, - { key: { startsWith: "weapon." } }, - { key: { startsWith: "armor." } }, - { key: { startsWith: "cape." } }, - ], - }, - }); - - let migratedCount = 0; - let deletedCount = 0; - let recreatedCount = 0; - - for (const item of nonStackableItems) { - const entry = await prisma.inventoryEntry.findUnique({ - where: { - userId_guildId_itemId: { userId, guildId, itemId: item.id }, - }, - }); - - if (!entry) continue; - - const props = (item.props as ItemProps | null) ?? {}; - const breakable = props.breakable; - const maxDurability = - breakable?.enabled !== false - ? breakable?.maxDurability ?? 100 - : undefined; - - const currentState = (entry.state as InventoryState | null) ?? {}; - const currentInstances = currentState.instances ?? []; - const currentQuantity = entry.quantity ?? 0; - - // Si tiene quantity>1 sin instances, está corrupto - if (currentQuantity > 1 && currentInstances.length === 0) { - // Opción 1: Migrar (convertir quantity a instances) - const newInstances: InventoryState["instances"] = []; - for (let i = 0; i < currentQuantity; i++) { - if (maxDurability && maxDurability > 0) { - newInstances.push({ durability: maxDurability }); - } else { - newInstances.push({}); - } - } - - await prisma.inventoryEntry.update({ - where: { id: entry.id }, - data: { - state: { - ...currentState, - instances: newInstances, - } as unknown as Prisma.InputJsonValue, - quantity: newInstances.length, - }, - }); - - migratedCount++; - } - // Si tiene quantity=1 pero sin instancia, crear instancia - else if (currentQuantity === 1 && currentInstances.length === 0) { - const newInstance = - maxDurability && maxDurability > 0 - ? { durability: maxDurability } - : {}; - - await prisma.inventoryEntry.update({ - where: { id: entry.id }, - data: { - state: { - ...currentState, - instances: [newInstance], - } as unknown as Prisma.InputJsonValue, - quantity: 1, - }, - }); - - migratedCount++; - } - // Si tiene instances pero sin durabilidad, inicializar - else if (currentInstances.length > 0 && maxDurability) { - let needsUpdate = false; - const fixedInstances = currentInstances.map((inst) => { - if (inst.durability == null) { - needsUpdate = true; - return { ...inst, durability: maxDurability }; - } - return inst; - }); - - if (needsUpdate) { - await prisma.inventoryEntry.update({ - where: { id: entry.id }, - data: { - state: { - ...currentState, - instances: fixedInstances, - } as unknown as Prisma.InputJsonValue, - quantity: fixedInstances.length, - }, - }); - migratedCount++; - } - } - } - - // Paso 2: Actualizar items en DB para asegurar stackable=false - const itemUpdateResult = await prisma.$executeRaw` - UPDATE "EconomyItem" - SET "stackable" = false - WHERE "key" LIKE 'tool.%' - OR "key" LIKE 'weapon.%' - OR "key" LIKE 'armor.%' - OR "key" LIKE 'cape.%' - `; - - await message.reply( - `✅ **Reseteo completado para <@${userId}>**\n` + - `• Entradas migradas: ${migratedCount}\n` + - `• Items actualizados en DB: ${itemUpdateResult}\n\n` + - `El usuario puede volver a usar sus herramientas normalmente.` - ); - } catch (error) { - console.error("Error en reset-inventory:", error); - await message.reply( - `❌ Error durante el reseteo: ${ - error instanceof Error ? error.message : "Desconocido" - }` - ); - } - }, -}; diff --git a/src/.backup/areaCreate.ts.backup2 b/src/.backup/areaCreate.ts.backup2 deleted file mode 100644 index 870377a..0000000 --- a/src/.backup/areaCreate.ts.backup2 +++ /dev/null @@ -1,101 +0,0 @@ -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, TextBasedChannel } from 'discord.js'; -import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10'; - -interface AreaState { - key: string; - name?: string; - type?: string; - config?: any; - metadata?: any; -} - -export const command: CommandMessage = { - name: 'area-crear', - type: 'message', - aliases: ['crear-area','areacreate'], - cooldown: 10, - description: 'Crea una GameArea (mina/laguna/arena/farm) para este servidor con editor.', - usage: 'area-crear ', - 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 key = args[0]?.trim(); - if (!key) { await message.reply('Uso: `!area-crear `'); return; } - - const guildId = message.guild!.id; - const exists = await prisma.gameArea.findFirst({ where: { key, guildId } }); - if (exists) { await message.reply('❌ Ya existe un área con esa key en este servidor.'); return; } - - const state: AreaState = { key, config: {}, metadata: {} }; - - const channel = message.channel as TextBasedChannel & { send: Function }; - const editorMsg = await channel.send({ - content: `🗺️ Editor de Área: \`${key}\``, - components: [ { type: 1, components: [ - { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'ga_base' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Config (JSON)', custom_id: 'ga_config' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'ga_meta' }, - { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'ga_save' }, - { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'ga_cancel' }, - ] } ], - }); - - const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i)=> i.user.id === message.author.id }); - collector.on('collect', async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - switch (i.customId) { - case 'ga_cancel': - await i.deferUpdate(); - await editorMsg.edit({ content: '❌ Editor de Área cancelado.', components: [] }); - collector.stop('cancel'); - return; - case 'ga_base': - await showBaseModal(i as ButtonInteraction, state); - return; - case 'ga_config': - await showJsonModal(i as ButtonInteraction, state, 'config', 'Config del Área'); - return; - case 'ga_meta': - await showJsonModal(i as ButtonInteraction, state, 'metadata', 'Meta del Área'); - return; - case 'ga_save': - if (!state.name || !state.type) { await i.reply({ content: '❌ Completa Base (nombre/tipo).', flags: MessageFlags.Ephemeral }); return; } - await prisma.gameArea.create({ data: { guildId, key: state.key, name: state.name!, type: state.type!, config: state.config ?? {}, metadata: state.metadata ?? {} } }); - await i.reply({ content: '✅ Área guardada.', flags: MessageFlags.Ephemeral }); - await editorMsg.edit({ content: `✅ Área \`${state.key}\` creada.`, components: [] }); - collector.stop('saved'); - return; - } - } catch (e) { - if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral }); - } - }); - - collector.on('end', async (_c,r)=> { if (r==='time') { try { await editorMsg.edit({ content: '⏰ Editor expirado.', components: [] }); } catch {} } }); - } -}; - -async function showBaseModal(i: ButtonInteraction, state: AreaState) { - const modal = { title: 'Base del Área', customId: 'ga_base_modal', components: [ - { type: ComponentType.Label, label: 'Nombre', component: { type: ComponentType.TextInput, customId: 'name', style: TextInputStyle.Short, required: true, value: state.name ?? '' } }, - { type: ComponentType.Label, label: 'Tipo (MINE/LAGOON/FIGHT/FARM)', component: { type: ComponentType.TextInput, customId: 'type', style: TextInputStyle.Short, required: true, value: state.type ?? '' } }, - ] } as const; - await i.showModal(modal); - try { const sub = await i.awaitModalSubmit({ time: 300_000 }); state.name = sub.components.getTextInputValue('name').trim(); state.type = sub.components.getTextInputValue('type').trim().toUpperCase(); await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral }); } catch {} -} - -async function showJsonModal(i: ButtonInteraction, state: AreaState, field: 'config'|'metadata', title: string) { - const current = JSON.stringify(state[field] ?? {}); - const modal = { title, customId: `ga_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 }); } } catch {} -} - diff --git a/src/.backup/areaEdit.ts.backup2 b/src/.backup/areaEdit.ts.backup2 deleted file mode 100644 index 4fa92d6..0000000 --- a/src/.backup/areaEdit.ts.backup2 +++ /dev/null @@ -1,101 +0,0 @@ -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, TextBasedChannel } from 'discord.js'; -import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10'; - -interface AreaState { - key: string; - name?: string; - type?: string; - config?: any; - metadata?: any; -} - -export const command: CommandMessage = { - name: 'area-editar', - type: 'message', - aliases: ['editar-area','areaedit'], - cooldown: 10, - description: 'Edita una GameArea de este servidor con un editor interactivo.', - usage: 'area-editar ', - 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 key = args[0]?.trim(); - if (!key) { await message.reply('Uso: `!area-editar `'); return; } - - const guildId = message.guild!.id; - const area = await prisma.gameArea.findFirst({ where: { key, guildId } }); - if (!area) { await message.reply('❌ No existe un área con esa key en este servidor.'); return; } - - const state: AreaState = { key, name: area.name, type: area.type, config: area.config ?? {}, metadata: area.metadata ?? {} }; - - const channel = message.channel as TextBasedChannel & { send: Function }; - const editorMsg = await channel.send({ - content: `🗺️ Editor de Área (editar): \`${key}\``, - components: [ { type: 1, components: [ - { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'ga_base' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Config (JSON)', custom_id: 'ga_config' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'ga_meta' }, - { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'ga_save' }, - { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'ga_cancel' }, - ] } ], - }); - - const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i)=> i.user.id === message.author.id }); - collector.on('collect', async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - switch (i.customId) { - case 'ga_cancel': - await i.deferUpdate(); - await editorMsg.edit({ content: '❌ Editor de Área cancelado.', components: [] }); - collector.stop('cancel'); - return; - case 'ga_base': - await showBaseModal(i as ButtonInteraction, state); - return; - case 'ga_config': - await showJsonModal(i as ButtonInteraction, state, 'config', 'Config del Área'); - return; - case 'ga_meta': - await showJsonModal(i as ButtonInteraction, state, 'metadata', 'Meta del Área'); - return; - case 'ga_save': - if (!state.name || !state.type) { await i.reply({ content: '❌ Completa Base (nombre/tipo).', flags: MessageFlags.Ephemeral }); return; } - await prisma.gameArea.update({ where: { id: area.id }, data: { name: state.name!, type: state.type!, config: state.config ?? {}, metadata: state.metadata ?? {} } }); - await i.reply({ content: '✅ Área actualizada.', flags: MessageFlags.Ephemeral }); - await editorMsg.edit({ content: `✅ Área \`${state.key}\` actualizada.`, components: [] }); - collector.stop('saved'); - return; - } - } catch (e) { - if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral }); - } - }); - - collector.on('end', async (_c,r)=> { if (r==='time') { try { await editorMsg.edit({ content: '⏰ Editor expirado.', components: [] }); } catch {} } }); - } -}; - -async function showBaseModal(i: ButtonInteraction, state: AreaState) { - const modal = { title: 'Base del Área', customId: 'ga_base_modal', components: [ - { type: ComponentType.Label, label: 'Nombre', component: { type: ComponentType.TextInput, customId: 'name', style: TextInputStyle.Short, required: true, value: state.name ?? '' } }, - { type: ComponentType.Label, label: 'Tipo (MINE/LAGOON/FIGHT/FARM)', component: { type: ComponentType.TextInput, customId: 'type', style: TextInputStyle.Short, required: true, value: state.type ?? '' } }, - ] } as const; - await i.showModal(modal); - try { const sub = await i.awaitModalSubmit({ time: 300_000 }); state.name = sub.components.getTextInputValue('name').trim(); state.type = sub.components.getTextInputValue('type').trim().toUpperCase(); await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral }); } catch {} -} - -async function showJsonModal(i: ButtonInteraction, state: AreaState, field: 'config'|'metadata', title: string) { - const current = JSON.stringify(state[field] ?? {}); - const modal = { title, customId: `ga_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 }); } } catch {} -} - diff --git a/src/.backup/createDisplayComponent.backup.ts b/src/.backup/createDisplayComponent.backup.ts deleted file mode 100644 index f2a25b4..0000000 --- a/src/.backup/createDisplayComponent.backup.ts +++ /dev/null @@ -1,618 +0,0 @@ -import { - ActionRowBuilder, - ButtonInteraction, - Message, - MessageComponentInteraction, - MessageFlags, - ModalBuilder, TextChannel, - TextInputBuilder, - TextInputStyle, -} from "discord.js"; -import logger from "../../../core/lib/logger"; -import {CommandMessage} from "../../../core/types/commands"; -import {listVariables} from "../../../core/lib/vars"; -import type Amayo from "../../../core/client"; -import {BlockState, DisplayComponentUtils, EditorActionRow} from "../../../core/types/displayComponentEditor"; -import type {DisplayComponentContainer} from "../../../core/types/displayComponents"; - -interface EditorData { - content?: string; - flags?: MessageFlags; - display?: DisplayComponentContainer; - components?: EditorActionRow[]; -} - -// Helper para actualizar el editor combinando Display Container dentro de components (tipado) -async function updateEditor(message: Message, data: EditorData): Promise { - const container = data.display; - const rows = Array.isArray(data.components) ? data.components : []; - const components = container ? [container, ...rows] : rows; - - const payload: any = { ...data }; - delete payload.display; - payload.components = components; - - if (payload.flags === undefined) { - payload.flags = MessageFlags.IsComponentsV2; - } - - await message.edit(payload); -} - -export const command: CommandMessage = { - name: "crear-embed", - type: "message", - aliases: ["embed-crear", "nuevo-embed", "blockcreatev2"], - cooldown: 20, - description: "Crea un nuevo bloque/embedded con editor interactivo (DisplayComponents).", - category: "Alianzas", - usage: "crear-embed ", - run: async (message, args, client) => { - if (!message.member?.permissions.has("Administrator")) { - await message.reply("❌ No tienes permisos de Administrador."); - return; - } - - const blockName = args[0]?.trim(); - if (!blockName) { - await message.reply("Debes proporcionar un nombre. Uso: `!crear-embed `"); - return; - } - - // Check if block name already exists - const existingBlock = await client.prisma.blockV2Config.findFirst({ - where: { - guildId: message.guild!.id, - name: blockName - } - }); - - if (existingBlock) { - await message.reply("❌ Ya existe un bloque con ese nombre!"); - return; - } - - // Estado inicial - let blockState: BlockState = { - title: `Editor de Block: ${blockName}`, - color: 0x5865f2, - coverImage: undefined, - components: [ - { type: 14, divider: false, spacing: 1 }, - { type: 10, content: "Usa los botones para configurar.", thumbnail: null } - ] - }; - - //@ts-ignore - const channelSend: If = message.channel; - if (!channelSend?.isTextBased()) { - await message.reply("❌ This command can only be used in a text-based channel."); - return; - } - - const editorMessage = await channelSend.send({ - content: "⚠️ **IMPORTANTE:** Prepara tus títulos, descripciones y URLs antes de empezar.\n" + - "Este editor usa **modales interactivos** y no podrás ver el chat mientras los usas.\n\n" + - "📝 **Recomendaciones:**\n" + - "• Ten preparados tus títulos y descripciones\n" + - "• Ten las URLs de imágenes listas para copiar\n" + - "• Los colores en formato HEX (#FF5733)\n" + - "• Las variables de usuario/servidor que necesites\n\n" + - "*Iniciando editor en 5 segundos...*" - }); - - // Esperar 5 segundos para que lean el mensaje - await new Promise(resolve => setTimeout(resolve, 5000)); - - // Actualizar para mostrar el editor - await updateEditor(editorMessage, { - content: undefined, - flags: MessageFlags.IsComponentsV2, - display: await DisplayComponentUtils.renderPreview(blockState, message.member!, message.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await handleEditorInteractions(editorMessage, message, client, blockName, blockState); - }, -}; - -async function handleEditorInteractions( - editorMessage: Message, - originalMessage: Message, - client: Amayo, - blockName: string, - blockState: BlockState -): Promise { - const collector = editorMessage.createMessageComponentCollector({ - time: 3600000, // 1 hour - filter: (interaction: MessageComponentInteraction) => interaction.user.id === originalMessage.author.id - }); - - collector.on("collect", async (interaction: ButtonInteraction) => { - try { - await handleButtonInteraction( - interaction, - editorMessage, - originalMessage, - client, - blockName, - blockState - ); - } catch (error) { - //@ts-ignore - logger.error("Error handling editor interaction:", error); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "❌ Ocurrió un error al procesar la interacción.", - flags: MessageFlags.Ephemeral - }); - } - } - }); - - collector.on("end", async (_collected, reason) => { - if (reason === "time") { - await handleEditorTimeout(editorMessage); - } - }); -} - -async function handleButtonInteraction( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - client: Amayo, - blockName: string, - blockState: BlockState -): Promise { - const { customId } = interaction; - - switch (customId) { - case "edit_title": - await handleEditTitle(interaction, editorMessage, originalMessage, blockState); - break; - - case "edit_description": - await handleEditDescription(interaction, editorMessage, originalMessage, blockState); - break; - - case "edit_color": - await handleEditColor(interaction, editorMessage, originalMessage, blockState); - break; - - case "add_content": - await handleAddContent(interaction, editorMessage, originalMessage, blockState); - break; - - case "add_separator": - await handleAddSeparator(interaction, editorMessage, originalMessage, blockState); - break; - - case "add_image": - await handleAddImage(interaction, editorMessage, originalMessage, blockState); - break; - - case "cover_image": - await handleCoverImage(interaction, editorMessage, originalMessage, blockState); - break; - - case "show_variables": - await handleShowVariables(interaction); - break; - - case "show_raw": - await handleShowRaw(interaction, blockState); - break; - - case "save_block": - await handleSaveBlock(interaction, client, blockName, blockState, originalMessage.guildId!); - break; - - case "cancel_block": - await handleCancelBlock(interaction, editorMessage); - break; - - default: - await interaction.reply({ - content: `⚠️ Funcionalidad \`${customId}\` en desarrollo.`, - flags: MessageFlags.Ephemeral - }); - break; - } -} - -async function handleEditTitle( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = new ModalBuilder() - .setCustomId("edit_title_modal") - .setTitle("Editar Título del Bloque"); - - const titleInput = new TextInputBuilder() - .setCustomId("title_input") - .setLabel("Título") - .setStyle(TextInputStyle.Short) - .setPlaceholder("Escribe el título del bloque...") - .setValue(blockState.title || "") - .setRequired(true) - .setMaxLength(256); - - const actionRow = new ActionRowBuilder().addComponents(titleInput); - modal.addComponents(actionRow); - - await interaction.showModal(modal); - - try { - const modalInteraction = await interaction.awaitModalSubmit({ time: 300000 }); - const newTitle = modalInteraction.fields.getTextInputValue("title_input").trim(); - - if (newTitle) { - blockState.title = newTitle; - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - } - - await modalInteraction.reply({ - content: "✅ Título actualizado correctamente.", - flags: MessageFlags.Ephemeral - }); - } catch { - // Modal timed out or error occurred - // no-op - } -} - -async function handleEditDescription( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = new ModalBuilder() - .setCustomId("edit_description_modal") - .setTitle("Editar Descripción del Bloque"); - - const descriptionInput = new TextInputBuilder() - .setCustomId("description_input") - .setLabel("Descripción") - .setStyle(TextInputStyle.Paragraph) - .setPlaceholder("Escribe la descripción del bloque...") - .setValue(blockState.description || "") - .setRequired(false) - .setMaxLength(4000); - - const actionRow = new ActionRowBuilder().addComponents(descriptionInput); - modal.addComponents(actionRow); - - await interaction.showModal(modal); - - try { - const modalInteraction = await interaction.awaitModalSubmit({ time: 300000 }); - const newDescription = modalInteraction.fields.getTextInputValue("description_input").trim(); - - blockState.description = newDescription || undefined; - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await modalInteraction.reply({ - content: "✅ Descripción actualizada correctamente.", - flags: MessageFlags.Ephemeral - }); - } catch { - // ignore - } -} - -async function handleEditColor( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = new ModalBuilder() - .setCustomId("edit_color_modal") - .setTitle("Editar Color del Bloque"); - - const colorInput = new TextInputBuilder() - .setCustomId("color_input") - .setLabel("Color (formato HEX)") - .setStyle(TextInputStyle.Short) - .setPlaceholder("#FF5733 o FF5733") - .setValue(blockState.color ? `#${blockState.color.toString(16).padStart(6, '0')}` : "") - .setRequired(false) - .setMaxLength(7); - - const actionRow = new ActionRowBuilder().addComponents(colorInput); - modal.addComponents(actionRow); - - await interaction.showModal(modal); - - try { - const modalInteraction = await interaction.awaitModalSubmit({ time: 300000 }); - const colorValue = modalInteraction.fields.getTextInputValue("color_input").trim(); - - if (colorValue) { - const cleanColor = colorValue.replace('#', ''); - const colorNumber = parseInt(cleanColor, 16); - - if (!isNaN(colorNumber) && cleanColor.length === 6) { - blockState.color = colorNumber; - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await modalInteraction.reply({ - content: "✅ Color actualizado correctamente.", - flags: MessageFlags.Ephemeral - }); - } else { - await modalInteraction.reply({ - content: "❌ Color inválido. Usa formato HEX como #FF5733", - flags: MessageFlags.Ephemeral - }); - } - } else { - blockState.color = undefined; - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await modalInteraction.reply({ - content: "✅ Color removido.", - flags: MessageFlags.Ephemeral - }); - } - } catch { - // ignore - } -} - -async function handleAddContent( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = new ModalBuilder() - .setCustomId("add_content_modal") - .setTitle("Añadir Contenido de Texto"); - - const contentInput = new TextInputBuilder() - .setCustomId("content_input") - .setLabel("Contenido") - .setStyle(TextInputStyle.Paragraph) - .setPlaceholder("Escribe el contenido de texto...") - .setRequired(true) - .setMaxLength(4000); - - const actionRow = new ActionRowBuilder().addComponents(contentInput); - modal.addComponents(actionRow); - - await interaction.showModal(modal); - - try { - const modalInteraction = await interaction.awaitModalSubmit({ time: 300000 }); - const content = modalInteraction.fields.getTextInputValue("content_input").trim(); - - if (content) { - blockState.components.push({ - type: 10, - content, - thumbnail: null - }); - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await modalInteraction.reply({ - content: "✅ Contenido añadido correctamente.", - flags: MessageFlags.Ephemeral - }); - } - } catch { - // ignore - } -} - -async function handleAddSeparator( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - blockState.components.push({ - type: 14, - divider: true, - spacing: 1 - }); - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await interaction.reply({ - content: "✅ Separador añadido correctamente.", - flags: MessageFlags.Ephemeral - }); -} - -async function handleAddImage( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = new ModalBuilder() - .setCustomId("add_image_modal") - .setTitle("Añadir Imagen"); - - const imageInput = new TextInputBuilder() - .setCustomId("image_input") - .setLabel("URL de la Imagen") - .setStyle(TextInputStyle.Short) - .setPlaceholder("https://ejemplo.com/imagen.png") - .setRequired(true) - .setMaxLength(512); - - const actionRow = new ActionRowBuilder().addComponents(imageInput); - modal.addComponents(actionRow); - - await interaction.showModal(modal); - - try { - const modalInteraction = await interaction.awaitModalSubmit({ time: 300000 }); - const imageUrl = modalInteraction.fields.getTextInputValue("image_input").trim(); - - if (imageUrl && DisplayComponentUtils.isValidUrl(imageUrl)) { - blockState.components.push({ - type: 12, - url: imageUrl - }); - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await modalInteraction.reply({ - content: "✅ Imagen añadida correctamente.", - ephemeral: true - }); - } else { - await modalInteraction.reply({ - content: "❌ URL de imagen inválida.", - ephemeral: true - }); - } - } catch { - // ignore - } -} - -async function handleCoverImage( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = new ModalBuilder() - .setCustomId("cover_image_modal") - .setTitle("Imagen de Portada"); - - const coverInput = new TextInputBuilder() - .setCustomId("cover_input") - .setLabel("URL de la Imagen de Portada") - .setStyle(TextInputStyle.Short) - .setPlaceholder("https://ejemplo.com/portada.png") - .setValue(blockState.coverImage || "") - .setRequired(false) - .setMaxLength(512); - - const actionRow = new ActionRowBuilder().addComponents(coverInput); - modal.addComponents(actionRow); - - await interaction.showModal(modal); - - try { - const modalInteraction = await interaction.awaitModalSubmit({ time: 300000 }); - const coverUrl = modalInteraction.fields.getTextInputValue("cover_input").trim(); - - if (coverUrl && DisplayComponentUtils.isValidUrl(coverUrl)) { - blockState.coverImage = coverUrl; - } else { - blockState.coverImage = undefined; - } - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await modalInteraction.reply({ - content: coverUrl ? "✅ Imagen de portada actualizada." : "✅ Imagen de portada removida.", - ephemeral: true - }); - } catch { - // ignore - } -} - -async function handleShowVariables(interaction: ButtonInteraction): Promise { - const variables = listVariables(); - await interaction.reply({ - content: `📋 **Variables disponibles:**\n\`\`\`\n${variables}\`\`\``, - flags: MessageFlags.Ephemeral - }); -} - -async function handleShowRaw(interaction: ButtonInteraction, blockState: BlockState): Promise { - const rawData = JSON.stringify(blockState, null, 2); - await interaction.reply({ - content: `📊 **Datos del bloque:**\n\`\`\`json\n${rawData.slice(0, 1800)}\`\`\``, - flags: MessageFlags.Ephemeral - }); -} - -async function handleSaveBlock( - interaction: ButtonInteraction, - client: Amayo, - blockName: string, - blockState: BlockState, - guildId: string -): Promise { - try { - await client.prisma.blockV2Config.create({ - data: { - guildId, - name: blockName, - config: blockState as any - } - }); - - await interaction.reply({ - content: `✅ **Bloque guardado exitosamente!**\n\n📄 **Nombre:** \`${blockName}\`\n🎨 **Componentes:** ${blockState.components.length}\n\n🎯 **Uso:** \`!send ${blockName}\``, - flags: MessageFlags.Ephemeral - }); - - logger.info(`Block created: ${blockName} in guild ${guildId}`); - } catch (error) { - //@ts-ignore - logger.error("Error saving block:", error); - await interaction.reply({ - content: "❌ Error al guardar el bloque. Inténtalo de nuevo.", - flags: MessageFlags.Ephemeral - }); - } -} - -async function handleCancelBlock(interaction: ButtonInteraction, editorMessage: Message): Promise { - await interaction.update({ - content: "❌ **Editor cancelado**\n\nLa creación del bloque ha sido cancelada.", - components: [], - embeds: [] - }); -} - -async function handleEditorTimeout(editorMessage: Message): Promise { - try { - await editorMessage.edit({ - content: "⏰ **Editor expirado**\n\nEl editor ha expirado por inactividad. Usa el comando nuevamente para crear un bloque.", - components: [], - embeds: [] - }); - } catch { - // message likely deleted - } -} diff --git a/src/.backup/createDisplayComponent.ts b/src/.backup/createDisplayComponent.ts deleted file mode 100644 index e1e2ea2..0000000 --- a/src/.backup/createDisplayComponent.ts +++ /dev/null @@ -1,1052 +0,0 @@ -import { - ButtonInteraction, - Message, - MessageComponentInteraction, - MessageFlags, - ModalSubmitInteraction, - TextChannel, -} from "discord.js"; -import { ComponentType, TextInputStyle, ButtonStyle } from "discord-api-types/v10"; -import logger from "../../../core/lib/logger"; -import {CommandMessage} from "../../../core/types/commands"; -import {listVariables} from "../../../core/lib/vars"; -import type Amayo from "../../../core/client"; -import { - BlockState, - DisplayComponentUtils, - EditorActionRow, - DESCRIPTION_PLACEHOLDER, - syncDescriptionComponent, - ensureDescriptionTextComponent, - normalizeDisplayContent -} from "../../../core/types/displayComponentEditor"; -import type {DisplayComponentContainer} from "../../../core/types/displayComponents"; -import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; - -interface EditorData { - content?: string; - flags?: MessageFlags; - display?: DisplayComponentContainer; - components?: EditorActionRow[]; -} - -// --- Helpers (yald-style minimal generators) --------------------------------- -type ModalField = { - customId: string; - style: number; - placeholder?: string; - value?: string; - required?: boolean; - maxLength?: number; - label?: string; -}; - -function createModal(params: { title: string; customId: string; fields: ModalField[] }) { - const components = params.fields.map(f => ({ - type: ComponentType.Label, - label: f.label ?? "", - component: { - type: ComponentType.TextInput, - customId: f.customId, - style: f.style, - placeholder: f.placeholder, - value: f.value, - required: f.required ?? false, - maxLength: f.maxLength - } - })); - return { title: params.title, customId: params.customId, components } as const; -} - -function buildSelectOptionsFromComponents(components: any[]) { - return components.map((c: any, idx: number) => ({ - label: c.type === 10 ? `Texto: ${c.content?.slice(0, 30) || '...'}` : c.type === 14 ? `Separador ${c.divider ? '(Visible)' : '(Invisible)'}` : c.type === 12 ? `Imagen: ${c.url?.slice(-30) || '...'}` : `Componente ${c.type}`, - value: String(idx), - description: c.type === 10 && (c.thumbnail || c.linkButton) ? (c.thumbnail ? 'Con thumbnail' : 'Con botón link') : undefined - })); -} - - -// Helper para actualizar el editor combinando Display Container dentro de components (tipado) -async function updateEditor(message: Message, data: EditorData): Promise { - const container = data.display; - const rows = Array.isArray(data.components) ? data.components : []; - const components = container ? [container, ...rows] : rows; - - const payload: any = { ...data }; - delete payload.display; - payload.components = components; - - if (payload.flags === undefined) { - payload.flags = MessageFlags.IsComponentsV2; - } - - // Si usamos Components V2, debemos limpiar explícitamente el content legado en el servidor - if (payload.flags === MessageFlags.IsComponentsV2) { - payload.content = null; - } - - await message.edit(payload); -} - -function stripLegacyDescriptionComponent(blockState: BlockState, match?: string | null): void { - if (!Array.isArray(blockState.components) || blockState.components.length === 0) return; - - const normalize = (value: string | undefined | null) => value?.replace(/\s+/g, " ").trim() ?? ""; - const target = normalize(match ?? blockState.description ?? undefined); - if (!target) return; - - const index = blockState.components.findIndex((component: any) => { - if (!component || component.type !== 10) return false; - if (component.thumbnail || component.linkButton) return false; - return normalize(component.content) === target; - }); - - if (index >= 0) { - blockState.components.splice(index, 1); - } -} - -export const command: CommandMessage = { - name: "crear-embed", - type: "message", - aliases: ["embed-crear", "nuevo-embed", "blockcreatev2"], - cooldown: 20, - description: "Crea un nuevo bloque/embedded con editor interactivo (DisplayComponents).", - category: "Alianzas", - usage: "crear-embed ", - run: async (message, args, client) => { - const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma); - if (!allowed) { - await message.reply("❌ No tienes permisos de ManageGuild ni rol de staff."); - return; - } - - const blockName = args[0]?.trim(); - if (!blockName) { - await message.reply("Debes proporcionar un nombre. Uso: `!crear-embed `"); - return; - } - - // Check if block name already exists - const existingBlock = await client.prisma.blockV2Config.findFirst({ - where: { - guildId: message.guild!.id, - name: blockName - } - }); - - if (existingBlock) { - await message.reply("❌ Ya existe un bloque con ese nombre!"); - return; - } - - // Estado inicial - let blockState: BlockState = { - title: `Editor de Block: ${blockName}`, - color: 0x5865f2, - coverImage: undefined, - components: [ - { type: 14, divider: false, spacing: 1 }, - { type: 10, content: DESCRIPTION_PLACEHOLDER, thumbnail: null } - ] - }; - - //@ts-ignore - const channelSend: If = message.channel; - if (!channelSend?.isTextBased()) { - await message.reply("❌ This command can only be used in a text-based channel."); - return; - } - - const editorMessage = await channelSend.send({ - content: "⚠️ **IMPORTANTE:** Prepara tus títulos, descripciones y URLs antes de empezar.\n" + - "Este editor usa **modales interactivos** y no podrás ver el chat mientras los usas.\n\n" + - "📝 **Recomendaciones:**\n" + - "• Ten preparados tus títulos y descripciones\n" + - "• Ten las URLs de imágenes listas para copiar\n" + - "• Los colores en formato HEX (#FF5733)\n" + - "• Las variables de usuario/servidor que necesites\n\n" + - "*Iniciando editor en 5 segundos...*" - }); - - // Esperar 5 segundos para que lean el mensaje - await new Promise(resolve => setTimeout(resolve, 5000)); - - // Actualizar para mostrar el editor - await updateEditor(editorMessage, { - content: undefined, - flags: MessageFlags.IsComponentsV2, - display: await DisplayComponentUtils.renderPreview(blockState, message.member!, message.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await handleEditorInteractions(editorMessage, message, client, blockName, blockState); - }, -}; - -async function handleEditorInteractions( - editorMessage: Message, - originalMessage: Message, - client: Amayo, - blockName: string, - blockState: BlockState -): Promise { - const collector = editorMessage.createMessageComponentCollector({ - time: 3600000, // 1 hour - filter: (interaction: MessageComponentInteraction) => interaction.user.id === originalMessage.author.id - }); - - collector.on("collect", async (interaction: MessageComponentInteraction) => { - // Verificar que sea una interacción de botón - if (!interaction.isButton()) return; - - try { - await handleButtonInteraction( - interaction, - editorMessage, - originalMessage, - client, - blockName, - blockState - ); - } catch (error) { - //@ts-ignore - logger.error("Error handling editor interaction:", error); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "❌ Ocurrió un error al procesar la interacción.", - flags: MessageFlags.Ephemeral - }); - } - } - }); - - collector.on("end", async (_collected, reason) => { - if (reason === "time") { - await handleEditorTimeout(editorMessage); - } - }); -} - -async function handleButtonInteraction( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - client: Amayo, - blockName: string, - blockState: BlockState -): Promise { - const { customId } = interaction; - - switch (customId) { - case "edit_title": - await handleEditTitle(interaction, editorMessage, originalMessage, blockState); - break; - - case "edit_description": - await handleEditDescription(interaction, editorMessage, originalMessage, blockState); - break; - - case "edit_color": - await handleEditColor(interaction, editorMessage, originalMessage, blockState); - break; - - case "add_content": - await handleAddContent(interaction, editorMessage, originalMessage, blockState); - break; - - case "add_separator": - await handleAddSeparator(interaction, editorMessage, originalMessage, blockState); - break; - - case "add_image": - await handleAddImage(interaction, editorMessage, originalMessage, blockState); - break; - - case "edit_thumbnail": { - ensureDescriptionTextComponent(blockState, { placeholder: DESCRIPTION_PLACEHOLDER }); - - const descriptionNormalized = normalizeDisplayContent(blockState.description); - const textDisplays = blockState.components - .map((component: any, idx: number) => ({ component, idx })) - .filter(({ component }) => component?.type === 10); - - if (textDisplays.length === 0) { - await interaction.deferReply({ flags: MessageFlags.Ephemeral }).catch(() => {}); - await interaction.editReply({ content: '❌ No hay bloques de texto disponibles para añadir thumbnail.' }).catch(() => {}); - break; - } - - const options = textDisplays.map(({ component, idx }) => ({ - label: descriptionNormalized && normalizeDisplayContent(component.content) === descriptionNormalized - ? 'Descripción principal' - : `Texto #${idx + 1}: ${component.content?.slice(0, 30) || '...'}`, - value: String(idx), - description: component.thumbnail ? 'Con thumbnail' : component.linkButton ? 'Con botón link' : 'Sin accesorio' - })); - - try { - await interaction.reply({ - flags: MessageFlags.Ephemeral, - content: 'Selecciona el bloque de texto al que quieres editar el thumbnail:', - components: [ - { - type: 1, - components: [ - { - type: 3, - custom_id: 'choose_text_for_thumbnail', - placeholder: 'Selecciona un bloque de texto', - options - } - ] - } - ] - }); - } catch (error) { - logger.error({ err: error }, 'Error enviando selector de thumbnails'); - break; - } - - let replyMsg: Message | null = null; - try { - replyMsg = await interaction.fetchReply(); - } catch {} - - if (!replyMsg) break; - - const selCollector = replyMsg.createMessageComponentCollector({ - componentType: ComponentType.StringSelect, - max: 1, - time: 60000, - filter: (it: any) => it.user.id === originalMessage.author.id - }); - - selCollector.on('collect', async (sel: any) => { - selCollector.stop('collected'); - - const idx = parseInt(sel.values[0], 10); - if (Number.isNaN(idx)) { - try { - if (!sel.replied && !sel.deferred) { - await sel.reply({ content: '❌ Selección inválida.', flags: MessageFlags.Ephemeral }); - } - } catch {} - return; - } - - const textComp = blockState.components[idx]; - if (!textComp || textComp.type !== 10) { - try { - if (!sel.replied && !sel.deferred) { - await sel.reply({ content: '❌ El bloque seleccionado ya no existe.', flags: MessageFlags.Ephemeral }); - } - } catch {} - return; - } - - const modal = createModal({ - title: '📎 Editar Thumbnail', - customId: `edit_thumbnail_modal_${idx}`, - fields: [ - { - customId: 'thumbnail_input', - style: TextInputStyle.Short, - placeholder: 'https://ejemplo.com/thumbnail.png (vacío para eliminar)', - value: textComp.thumbnail || '', - maxLength: 512, - required: false, - label: 'URL del Thumbnail' - } - ] - }); - - try { - await sel.showModal(modal); - } catch (error) { - logger.error({ err: error }, 'No se pudo mostrar el modal de thumbnail'); - return; - } - - const modalInteraction = await awaitModalWithDeferredReply(sel); - if (!modalInteraction) return; - - const rawInput = modalInteraction.components.getTextInputValue('thumbnail_input').trim(); - - if (rawInput.length === 0) { - textComp.thumbnail = null; - await modalInteraction.editReply({ content: '✅ Thumbnail eliminado.' }).catch(() => {}); - } else if (!DisplayComponentUtils.isValidUrl(rawInput)) { - await modalInteraction.editReply({ content: '❌ URL de thumbnail inválida.' }).catch(() => {}); - return; - } else if (textComp.linkButton) { - await modalInteraction.editReply({ content: '❌ Este bloque tiene un botón link. Elimínalo antes de añadir un thumbnail.' }).catch(() => {}); - return; - } else { - textComp.thumbnail = rawInput; - await modalInteraction.editReply({ content: '✅ Thumbnail actualizado.' }).catch(() => {}); - } - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - }); - - selCollector.on('end', async () => { - try { - await replyMsg!.edit({ components: [] }); - } catch {} - }); - - break; - } - - case "cover_image": - await handleCoverImage(interaction, editorMessage, originalMessage, blockState); - break; - - case "move_block": { - const options = buildSelectOptionsFromComponents(blockState.components); - - await interaction.reply({ - flags: MessageFlags.Ephemeral, - content: 'Selecciona el bloque que quieres mover:', - components: [ - { type: 1, components: [ { type: 3, custom_id: 'move_block_select', placeholder: 'Elige un bloque', options } ] }, - ], - }); - const replyMsg = await interaction.fetchReply(); - // @ts-ignore - const selCollector = replyMsg.createMessageComponentCollector({ componentType: ComponentType.StringSelect, max: 1, time: 60000, filter: (it: any) => it.user.id === originalMessage.author.id }); - selCollector.on('collect', async (sel: any) => { - const idx = parseInt(sel.values[0]); - await sel.update({ - content: '¿Quieres mover este bloque?', - components: [ - { type: 1, components: [ - { type: 2, style: ButtonStyle.Secondary, label: '⬆️ Subir', custom_id: `move_up_${idx}`, disabled: idx === 0 }, - { type: 2, style: ButtonStyle.Secondary, label: '⬇️ Bajar', custom_id: `move_down_${idx}`, disabled: idx === blockState.components.length - 1 }, - ]}, - ], - }); - // @ts-ignore - const btnCollector = replyMsg.createMessageComponentCollector({ componentType: ComponentType.Button, max: 1, time: 60000, filter: (b: any) => b.user.id === originalMessage.author.id }); - btnCollector.on('collect', async (b: any) => { - if (b.customId.startsWith('move_up_')) { - const i2 = parseInt(b.customId.replace('move_up_', '')); - if (i2 > 0) { - const item = blockState.components[i2]; - blockState.components.splice(i2, 1); - blockState.components.splice(i2 - 1, 0, item); - } - await b.update({ content: '✅ Bloque movido arriba.', components: [] }); - } else if (b.customId.startsWith('move_down_')) { - const i2 = parseInt(b.customId.replace('move_down_', '')); - if (i2 < blockState.components.length - 1) { - const item = blockState.components[i2]; - blockState.components.splice(i2, 1); - blockState.components.splice(i2 + 1, 0, item); - } - await b.update({ content: '✅ Bloque movido abajo.', components: [] }); - } - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false), - }); - btnCollector.stop(); - selCollector.stop(); - }); - }); - break; - } - - case "delete_block": { - const options: any[] = []; - if (blockState.coverImage) options.push({ label: '🖼️ Imagen de Portada', value: 'cover_image', description: 'Imagen principal del bloque' }); - options.push(...buildSelectOptionsFromComponents(blockState.components)); - - if (options.length === 0) { - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - // @ts-ignore - await interaction.editReply({ content: '❌ No hay elementos para eliminar.' }); - break; - } - - await interaction.reply({ - flags: MessageFlags.Ephemeral, - content: 'Selecciona el elemento que quieres eliminar:', - components: [ - { type: 1, components: [ { type: 3, custom_id: 'delete_block_select', placeholder: 'Elige un elemento', options } ] }, - ], - }); - const replyMsg = await interaction.fetchReply(); - // @ts-ignore - const selCollector = replyMsg.createMessageComponentCollector({ componentType: ComponentType.StringSelect, max: 1, time: 60000, filter: (it: any) => it.user.id === originalMessage.author.id }); - selCollector.on('collect', async (sel: any) => { - const selectedValue = sel.values[0]; - if (selectedValue === 'cover_image') { - // @ts-ignore - blockState.coverImage = null; - await sel.update({ content: '✅ Imagen de portada eliminada.', components: [] }); - } else { - const idx = parseInt(selectedValue); - blockState.components.splice(idx, 1); - await sel.update({ content: '✅ Elemento eliminado.', components: [] }); - } - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false), - }); - selCollector.stop(); - }); - break; - } - - case "show_variables": - await handleShowVariables(interaction); - break; - - case "show_raw": - await handleShowRaw(interaction, blockState); - break; - - case "save_block": - await handleSaveBlock(interaction, editorMessage, client, blockName, blockState, originalMessage.guildId!); - break; - - case "cancel_block": - await handleCancelBlock(interaction, editorMessage); - break; - - default: - await interaction.reply({ - content: `⚠️ Funcionalidad \`${customId}\` en desarrollo.`, - flags: MessageFlags.Ephemeral, - }); - break; - } -} - -async function awaitModalWithDeferredReply( - interaction: ButtonInteraction | MessageComponentInteraction, - options: Parameters[0] = { time: 300000 } -): Promise { - try { - const modalInteraction = await interaction.awaitModalSubmit(options); - if (!modalInteraction.deferred && !modalInteraction.replied) { - await modalInteraction.deferReply({ flags: MessageFlags.Ephemeral }); - } - return modalInteraction; - } catch (error) { - if (!(error instanceof Error) || !error.message.includes('Collector received no interactions')) { - logger.error({ err: error }, "Error esperando envío de modal en editor"); - } - return null; - } -} - -async function handleEditTitle( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = createModal({ - title: "Editar Título del Bloque", - customId: "edit_title_modal", - fields: [ - { - customId: "title_input", - style: TextInputStyle.Short, - required: true, - placeholder: "Escribe el título del bloque...", - value: blockState.title || "", - maxLength: 256, - label: 'Título' - } - ] - }); - - await interaction.showModal(modal as any); - - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; - - const newTitle = modalInteraction.components.getTextInputValue("title_input").trim(); - - if (newTitle) { - blockState.title = newTitle; - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - } - - await modalInteraction.editReply({ - content: "✅ Título actualizado correctamente." - }); - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction.editReply({ - content: "❌ No se pudo actualizar el título. Inténtalo de nuevo." - }).catch(() => {}); - } - if (error instanceof Error && error.message.includes('Collector received no interactions')) { - return; - } - logger.error({ err: error }, "Error procesando modal de título"); - } -} - -async function handleEditDescription( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = { - title: "Editar Descripción del Bloque", - customId: "edit_description_modal", - components: [ - { - type: ComponentType.Label, - label: "Descripción", - component: { - type: ComponentType.TextInput, - customId: "description_input", - style: TextInputStyle.Paragraph, - required: false, - placeholder: "Escribe la descripción del bloque...", - value: blockState.description || "", - maxLength: 4000 - } - } - ] - } as const; - - await interaction.showModal(modal); - - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; - - const rawDescription = modalInteraction.components.getTextInputValue("description_input"); - const previousDescription = typeof blockState.description === "string" ? blockState.description : null; - syncDescriptionComponent(blockState, rawDescription, { - previousDescription, - placeholder: DESCRIPTION_PLACEHOLDER - }); - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await modalInteraction.editReply({ - content: "✅ Descripción actualizada correctamente." - }); - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction.editReply({ - content: "❌ No se pudo actualizar la descripción. Inténtalo de nuevo." - }).catch(() => {}); - } - if (error instanceof Error && error.message.includes('Collector received no interactions')) { - return; - } - logger.error({ err: error }, "Error procesando modal de descripción"); - } -} - -async function handleEditColor( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = { - title: "Editar Color del Bloque", - customId: "edit_color_modal", - components: [ - { - type: ComponentType.Label, - label: "Color (formato HEX)", - component: { - type: ComponentType.TextInput, - customId: "color_input", - style: TextInputStyle.Short, - required: false, - placeholder: "#FF5733 o FF5733", - value: blockState.color ? `#${blockState.color.toString(16).padStart(6, '0')}` : "", - maxLength: 7 - } - } - ] - } as const; - - await interaction.showModal(modal); - - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; - - const colorValue = modalInteraction.components.getTextInputValue("color_input").trim(); - - if (colorValue) { - const cleanColor = colorValue.replace('#', ''); - const colorNumber = parseInt(cleanColor, 16); - - if (!isNaN(colorNumber) && cleanColor.length === 6) { - blockState.color = colorNumber; - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await modalInteraction.editReply({ - content: "✅ Color actualizado correctamente." - }); - } else { - await modalInteraction.editReply({ - content: "❌ Color inválido. Usa formato HEX como #FF5733" - }); - } - } else { - blockState.color = undefined; - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await modalInteraction.editReply({ - content: "✅ Color removido." - }); - } - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction.editReply({ - content: "❌ No se pudo actualizar el color. Inténtalo de nuevo." - }).catch(() => {}); - } - if (error instanceof Error && error.message.includes('Collector received no interactions')) { - return; - } - logger.error({ err: error }, "Error procesando modal de color"); - } -} - -async function handleAddContent( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = { - title: "Añadir Contenido de Texto", - customId: "add_content_modal", - components: [ - { - type: ComponentType.Label, - label: "Contenido", - component: { - type: ComponentType.TextInput, - customId: "content_input", - style: TextInputStyle.Paragraph, - required: true, - placeholder: "Escribe el contenido de texto...", - maxLength: 4000 - } - } - ] - } as const; - - await interaction.showModal(modal); - - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; - - const content = modalInteraction.components.getTextInputValue("content_input").trim(); - - if (content) { - blockState.components.push({ - type: 10, - content, - thumbnail: null - }); - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await modalInteraction.editReply({ - content: "✅ Contenido añadido correctamente." - }); - } - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction.editReply({ - content: "❌ No se pudo añadir el contenido. Inténtalo de nuevo." - }).catch(() => {}); - } - if (error instanceof Error && error.message.includes('Collector received no interactions')) { - return; - } - logger.error({ err: error }, "Error procesando modal de contenido"); - } -} - -async function handleAddSeparator( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const wasAcknowledged = interaction.deferred || interaction.replied; - if (!wasAcknowledged) { - try { - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - } catch (error) { - logger.warn({ err: error }, "No se pudo diferir respuesta al añadir separador"); - } - } - - blockState.components.push({ - type: 14, - divider: true, - spacing: 1 - }); - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - const payload = { content: "✅ Separador añadido correctamente.", flags: MessageFlags.Ephemeral } as const; - - if (interaction.deferred) { - await interaction.editReply({ content: payload.content }).catch(() => {}); - } else if (interaction.replied) { - await interaction.followUp(payload).catch(() => {}); - } else { - await interaction.reply(payload).catch(() => {}); - } -} - -async function handleAddImage( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = createModal({ - title: "Añadir Imagen", - customId: "add_image_modal", - fields: [ - { - customId: "image_input", - style: TextInputStyle.Short, - required: true, - placeholder: "https://ejemplo.com/imagen.png", - maxLength: 512, - label: 'URL de la Imagen' - } - ] - }); - - await interaction.showModal(modal as any); - - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; - - const imageUrl = modalInteraction.components.getTextInputValue("image_input").trim(); - - if (imageUrl && DisplayComponentUtils.isValidUrl(imageUrl)) { - blockState.components.push({ - type: 12, - url: imageUrl - }); - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await modalInteraction.editReply({ - content: "✅ Imagen añadida correctamente." - }); - } else { - await modalInteraction.editReply({ - content: "❌ URL de imagen inválida." - }); - } - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction.editReply({ - content: "❌ No se pudo añadir la imagen. Inténtalo de nuevo." - }).catch(() => {}); - } - if (error instanceof Error && error.message.includes('Collector received no interactions')) { - return; - } - logger.error({ err: error }, "Error procesando modal de imagen"); - } -} - -async function handleCoverImage( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = { - title: "Imagen de Portada", - customId: "cover_image_modal", - components: [ - { - type: ComponentType.Label, - label: "URL de la Imagen de Portada", - component: { - type: ComponentType.TextInput, - customId: "cover_input", - style: TextInputStyle.Short, - required: false, - placeholder: "https://ejemplo.com/portada.png", - value: blockState.coverImage || "", - maxLength: 512 - } - } - ] - } as const; - - await interaction.showModal(modal); - - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; - - const coverUrl = modalInteraction.components.getTextInputValue("cover_input").trim(); - - if (coverUrl && DisplayComponentUtils.isValidUrl(coverUrl)) { - blockState.coverImage = coverUrl; - } else { - blockState.coverImage = undefined; - } - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await modalInteraction.editReply({ - content: coverUrl ? "✅ Imagen de portada actualizada." : "✅ Imagen de portada removida." - }); - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction.editReply({ - content: "❌ No se pudo actualizar la imagen de portada. Inténtalo de nuevo." - }).catch(() => {}); - } - if (error instanceof Error && error.message.includes('Collector received no interactions')) { - return; - } - logger.error({ err: error }, "Error procesando modal de portada"); - } -} - -async function handleShowVariables(interaction: ButtonInteraction): Promise { - const variables = listVariables(); - await interaction.reply({ - content: `📋 **Variables disponibles:**\n\`\`\`\n${variables}\`\`\``, - flags: MessageFlags.Ephemeral - }); -} - -async function handleShowRaw(interaction: ButtonInteraction, blockState: BlockState): Promise { - const rawData = JSON.stringify(blockState, null, 2); - await interaction.reply({ - content: `📊 **Datos del bloque:**\n\`\`\`json\n${rawData.slice(0, 1800)}\`\`\``, - flags: MessageFlags.Ephemeral - }); -} - -async function handleSaveBlock( - interaction: ButtonInteraction, - editorMessage: Message, - client: Amayo, - blockName: string, - blockState: BlockState, - guildId: string -): Promise { - try { - stripLegacyDescriptionComponent(blockState); - await client.prisma.blockV2Config.create({ - data: { - guildId, - name: blockName, - config: blockState as any - } - }); - - await interaction.reply({ - content: `✅ **Bloque guardado exitosamente!**\n\n📄 **Nombre:** \`${blockName}\`\n🎨 **Componentes:** ${blockState.components.length}\n\n🎯 **Uso:** \`!send-embed ${blockName}\``, - flags: MessageFlags.Ephemeral - }); - - // Cerrar el editor eliminando el mensaje del editor - try { await editorMessage.delete(); } catch {} - } catch (error) { - //@ts-ignore - logger.error("Error saving block:", error); - await interaction.reply({ - content: "❌ Error al guardar el bloque. Inténtalo de nuevo.", - flags: MessageFlags.Ephemeral - }); - } -} - -async function handleCancelBlock(interaction: ButtonInteraction, editorMessage: Message): Promise { - try { - await interaction.deferUpdate(); - } catch {} - await updateEditor(editorMessage, { - display: { - type: 17, - components: [ - { type: 10, content: "❌ **Editor cancelado**" }, - { type: 10, content: "La creación del bloque ha sido cancelada." } - ] - } as any, - components: [] - }); -} - -async function handleEditorTimeout(editorMessage: Message): Promise { - try { - await updateEditor(editorMessage, { - display: { - type: 17, - components: [ - { type: 10, content: "⏰ **Editor expirado**" }, - { type: 10, content: "El editor ha expirado por inactividad. Usa el comando nuevamente para crear un bloque." } - ] - } as any, - components: [] - }); - } catch { - // message likely deleted - } -} diff --git a/src/.backup/createEmbed.backup.ts b/src/.backup/createEmbed.backup.ts deleted file mode 100644 index 0103240..0000000 --- a/src/.backup/createEmbed.backup.ts +++ /dev/null @@ -1,494 +0,0 @@ -import {CommandMessage} from "../core/types/commands"; - -import { - ActionRowBuilder, - ButtonBuilder, - //@ts-ignore - ButtonStyle, - //@ts-ignore - ChannelType, - //@ts-ignore - ComponentType, - EmbedBuilder, - TextChannel -} from "discord.js"; -import {replaceVars} from "../core/lib/vars"; - - - -/** - * VARIABLES COMPONENTS V2 - */ - - const variables_text = { - "flags": 32768, - "components": [ - { - "type": 17, - "components": [ - { - "type": 10, - "content": "﹒⌒    Variables Text    ╰୧﹒" - }, - { - "type": 14, - "spacing": 1, - "divider": false - }, - { - "type": 10, - "content": "**✿ ૮ ․ ․ ྀིა User Var**" - }, - { - "type": 10, - "content": "\n(user.id) **-** 𝑀𝑢𝑒𝑠𝑡𝑟𝑎 𝑒𝑙 𝑖𝑑𝑒𝑛𝑡𝑖𝑓𝑖𝑐𝑎𝑑𝑜𝑟 𝑑𝑒 𝑢𝑛 𝑢𝑠𝑢𝑎𝑟𝑖𝑜.\n(user.name) **-** 𝑀𝑢𝑒𝑠𝑡𝑟𝑎 𝑒𝑙 𝑛𝑜𝑚𝑏𝑟𝑒 𝑑𝑒 𝑢𝑛 𝑢𝑠𝑢𝑎𝑟𝑖𝑜\n(user.avatar) **-** 𝑀𝑢𝑒𝑠𝑡𝑟𝑎 𝑙𝑎 𝑢𝑟𝑙 𝑑𝑒𝑙 𝑎𝑣𝑎𝑡𝑎𝑟 𝑑𝑒𝑙 𝑢𝑠𝑢𝑎𝑟𝑖𝑜.\n(user.mention) **-** 𝑀𝑒𝑛𝑐𝑖𝑜𝑛𝑎 𝑎 𝑢𝑛 𝑢𝑠𝑢𝑎𝑟𝑖𝑜 𝑐𝑜𝑛 𝑠𝑢 @" - }, - { - "type": 10, - "content": "**✿ ૮ ․ ․ ྀིა Guild Var**" - }, - { - "type": 10, - "content": "(guild.icon) **-** 𝑀𝑢𝑒𝑠𝑡𝑟𝑎 𝑒𝑙 𝑖𝑐𝑜𝑛𝑜 𝑑𝑒𝑙 𝑠𝑒𝑟𝑣𝑖𝑑𝑜𝑟.\n(guild.name) **-** 𝑀𝑢𝑒𝑠𝑡𝑟𝑎 𝑒𝑙 𝑛𝑜𝑚𝑏𝑟𝑒 𝑑𝑒𝑙 𝑠𝑒𝑟𝑣𝑖𝑑𝑜𝑟.\n" - } - ], - "accent_color": null, - "spoiler": true - } - ] -} - -/** - * COMMAND EXECUTE - */ - -export const command: CommandMessage = { - name: "embedcreate", - type: "message", - aliases: ["crearembed", "newembed"], - cooldown: 20, - // @ts-ignore - run: async (message, args, client) => { - if (!message.member?.permissions.has("Administrator")) { - return message.reply("❌ No tienes permisos de Administrador."); - } - - const embedName: string | null = args[0] ?? null; - if (!embedName) { - return message.reply( - "Debes proporcionar un nombre para el embed. Uso: `!embedcreate `" - ); - } - - const nameIsValid = await client.prisma.embedConfig.findFirst({ where: { - //@ts-ignore - guildId: message.guild.id, - name: embedName - }}) - if(nameIsValid) return message.reply("❌ Nombre del embed ya fue tomado!") - - // 📌 Estado independiente - let embedState: { - title?: string; - description?: string; - color?: number; - imageUrl?: string; - thumbnail?: string; - footer?: string; - } = { - title: `Editor de Embed: ${embedName}`, - description: - "Usa los botones de abajo para configurar este embed.\n\n_Ejemplo de variable: `{user.name}`_", - color: 0x5865f2, - footer: "Haz clic en Guardar cuando termines.", - thumbnail: `${message.guild!.iconURL()}`, - imageUrl: `https://i.pinimg.com/originals/d2/c3/79/d2c3798684709cef3ed532b59c59bad4.gif` - }; - - // 📌 Función para construir un embed a partir del estado - const renderPreview = async () => { - const preview = new EmbedBuilder() - .setColor(embedState.color ?? 0x5865f2); - - if (embedState.title) - preview.setTitle( - //@ts-ignore - await replaceVars(embedState.title, message.member, message.guild) - ); - if (embedState.description) - preview.setDescription( - //@ts-ignore - await replaceVars(embedState.description, message.member, message.guild) - ); - if (embedState.footer) - preview.setFooter({ - //@ts-ignore - text: await replaceVars(embedState.footer, message.member, message.guild), - }); - if (embedState.imageUrl) - preview.setImage( - //@ts-ignore - await replaceVars(embedState.imageUrl, message.member, message.guild) - ); - if (embedState.thumbnail) - preview.setThumbnail( - //@ts-ignore - await replaceVars(embedState.thumbnail, message.member, message.guild) - ) - - return preview; - }; - - // 📌 Botones - const generateButtonRows = (disabled = false) => { - const primaryRow = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("edit_title") - .setLabel("Título") - .setStyle(ButtonStyle.Primary) - .setDisabled(disabled), - new ButtonBuilder() - .setCustomId("edit_description") - .setLabel("Descripción") - .setStyle(ButtonStyle.Primary) - .setDisabled(disabled), - new ButtonBuilder() - .setCustomId("edit_color") - .setLabel("Color") - .setStyle(ButtonStyle.Primary) - .setDisabled(disabled), - new ButtonBuilder() - .setCustomId('edit_imageurl') - .setLabel('Image') - .setStyle(ButtonStyle.Secondary) - .setDisabled(disabled), - new ButtonBuilder() - .setCustomId('edit_thumbnail') - .setLabel('Thumbnail') - .setStyle(ButtonStyle.Secondary) - .setDisabled(disabled) - ); - - const secondaryRow = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("edit_footer") - .setLabel("Footer") - .setStyle(ButtonStyle.Secondary) - .setDisabled(disabled) - ); - - const controlRow = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("save_embed") - .setLabel("Guardar") - .setStyle(ButtonStyle.Success) - .setDisabled(disabled), - new ButtonBuilder() - .setCustomId("cancel_embed") - .setLabel("Cancelar") - .setStyle(ButtonStyle.Danger) - .setDisabled(disabled) - ); - - return [primaryRow, secondaryRow, controlRow]; - }; - - /** - * Botones Custom - */ - - const btns = (disabled = false) => ({ - flags: 32768, - components: [ - { - type: 17, - components: [ - { - type: 10, - content: "﹒⌒    Options    ╰୧﹒" - }, - { - type: 14, - divider: true - }, - { - type: 1, - components: [ - { - style: 2, - type: 2, - label: "Titulo", - disabled: disabled, // 👈 aquí ya funciona - custom_id: "edit_title" - }, - { - style: 2, - type: 2, - label: "Descripción", - disabled: disabled, - custom_id: "edit_description" - }, - { - style: 2, - type: 2, - label: "Color", - disabled: disabled, - custom_id: "edit_color" - }, - { - style: 2, - type: 2, - label: "Imagen", - disabled: disabled, - custom_id: 'edit_imageurl' - }, - { - style: 2, - type: 2, - label: "Thumbnail", - disabled: disabled, - custom_id: 'edit_thumbnail' - } - ] - }, - { - type: 1, - components: [ - { - style: 2, - type: 2, - label: "Footer", - disabled: disabled, - custom_id: "edit_footer" - } - ] - }, - { - type: 1, - components: [ - { - style: 3, - type: 2, - label: "Guardar", - disabled: disabled, - custom_id: "save_embed" - }, - { - style: 4, - type: 2, - label: "Eliminar", - disabled: disabled, - custom_id: "cancel_embed" - } - ] - } - ], - accent_color: null - } - ] - }); - - - - if (message.channel.type === ChannelType.GuildText) { - const channel = message.channel as TextChannel; - - const editorMessage = await channel.send({ - embeds: [await renderPreview()], - //components: generateButtonRows(), - }); - - const collector = editorMessage.createMessageComponentCollector({ - componentType: ComponentType.Button, - time: 300000, - }); - - collector.on("collect", async (i) => { - if (i.user.id !== message.author.id) { - await i.reply({ - content: "No puedes usar este menú.", - ephemeral: true, - }); - return; - } - await i.deferUpdate(); - await editorMessage.edit({ components: generateButtonRows(true) }); - - // Guardar - if (i.customId === "save_embed") { - try { - const dataForDb = { - title: embedState.title, - description: embedState.description, - color: embedState.color ? `#${embedState.color.toString(16).padStart(6, '0')}` : null, - footerText: embedState.footer, - }; - - await client.prisma.embedConfig.upsert({ - where: { - guildId_name: { - guildId: message.guildId!, - name: embedName, - }, - }, - update: dataForDb, - create: { - name: embedName, - ...dataForDb, - // ✅ ESTA ES LA SOLUCIÓN: - // Le decimos a Prisma que se conecte al Guild o lo cree si no existe. - guild: { - connectOrCreate: { - where: { id: message.guildId! }, - create: { - id: message.guildId!, - name: message.guild!.name, // Asegura que el nombre del servidor se guarde - }, - }, - }, - }, - }); - - const saved = new EmbedBuilder() - .setColor(0x00ff00) - .setTitle(`✅ Guardado: ${embedName}`) - .setDescription("La configuración se guardó en la base de datos."); - - await editorMessage.edit({ - embeds: [saved], - components: [], - }); - } catch (e) { - const errorEmbed = new EmbedBuilder() - .setColor(0xff0000) - .setTitle("❌ Error al Guardar") - .setDescription("No se pudo guardar en la base de datos. Revisa la consola."); - await editorMessage.edit({ - embeds: [errorEmbed], - components: [], - }); - console.error("Error de Prisma al guardar el embed:", e); - } - collector.stop(); - return; - } - // Cancelar - if (i.customId === "cancel_embed") { - await editorMessage.delete(); - collector.stop(); - return; - } - - // Edición - let promptContent = ""; - let variableContent; - let fieldToEdit: "title" | "description" | "color" | "footer" | "image" | "thumbnail" | null = - null; - - switch (i.customId) { - case "edit_title": - promptContent = - "Escribe el nuevo **título** (puedes usar variables como `(guild.name)`)."; - variableContent = variables_text - fieldToEdit = "title"; - break; - case "edit_description": - promptContent = - "Escribe la nueva **descripción** (puedes usar variables)."; - variableContent = variables_text - fieldToEdit = "description"; - break; - case "edit_color": - promptContent = - "Escribe el nuevo **color** en formato hexadecimal (ej: `#FF0000`)."; - variableContent = variables_text - fieldToEdit = "color"; - break; - case "edit_footer": - promptContent = - "Escribe el nuevo **texto del footer** (puedes usar variables)."; - variableContent = variables_text - fieldToEdit = "footer"; - break; - case "edit_imageurl": - promptContent = - "Pega el url **de la imagen** (puedes usar variables)."; - variableContent = variables_text - fieldToEdit = "image"; - break; - case "edit_thumbnail": - promptContent = - "Pega el url **del thumbnail** (puedes usar variables)."; - variableContent = variables_text - fieldToEdit = "thumbnail"; - break; - } - - //@ts-ignore - const variableMessage = await i.channel.send(variableContent) - //@ts-ignore - const promptMessage = await i.channel.send(promptContent); - - //@ts-ignore - const messageCollector = i.channel!.createMessageCollector({ - //@ts-ignore - filter: (m: Message) => m.author.id === i.user.id, - max: 1, - time: 60000, - }); - //@ts-ignore - messageCollector.on("collect", async (collectedMessage) => { - const newValue = collectedMessage.content; - - if (fieldToEdit === "title") embedState.title = newValue; - if (fieldToEdit === "description") embedState.description = newValue; - if (fieldToEdit === "footer") embedState.footer = newValue; - // added v0.0.1.1 - if (fieldToEdit === "image") embedState.imageUrl = newValue; - if (fieldToEdit === "thumbnail") embedState.thumbnail = newValue; - - if (fieldToEdit === "color") { - try { - const hex = newValue.replace("#", ""); - embedState.color = parseInt(hex, 16); - } catch { - embedState.color = 0x5865f2; - } - } - - await collectedMessage.delete(); - await promptMessage.delete(); - await variableMessage.delete(); - - await editorMessage.edit({ - embeds: [await renderPreview()], - components: generateButtonRows(false), - }); - }); - //@ts-ignore - messageCollector.on("end", async (collected) => { - if (collected.size === 0) { - await promptMessage.delete(); - await variableMessage.delete(); - await editorMessage.edit({ - components: generateButtonRows(false), - }); - } - }); - }); - - collector.on("end", async (_, reason) => { - if (reason === "time") { - const timeoutEmbed = new EmbedBuilder() - .setColor(0xff0000) - .setTitle("Editor finalizado por inactividad."); - - await editorMessage.edit({ - embeds: [timeoutEmbed], - components: [], - }); - } - }); - } - }, -}; diff --git a/src/.backup/editEmbed.backup.ts b/src/.backup/editEmbed.backup.ts deleted file mode 100644 index ea63ba3..0000000 --- a/src/.backup/editEmbed.backup.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { CommandMessage } from "../core/types/commands"; -// @ts-ignore -import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, TextChannel, ChannelType } from "discord.js"; -//@ts-ignore -import { ButtonStyle, ComponentType } from "discord.js"; -import { replaceVars } from "../core/lib/vars"; - -export const command: CommandMessage = { - name: "editembed", - type: "message", - aliases: ["modembed", "updateembed"], - cooldown: 20, - // @ts-ignore - run: async (message, args, client) => { - if (!message.member?.permissions.has("Administrator")) { - return message.reply("❌ No tienes permisos de Administrador."); - } - - const embedName: string | null = args[0] ?? null; - if (!embedName) { - return message.reply( - "Debes proporcionar un nombre para el embed. Uso: `!editembed `" - ); - } - - // 📌 Buscar en la base de datos - const existing = await client.prisma.embedConfig.findUnique({ - where: { - guildId_name: { - guildId: message.guildId!, - name: embedName, - }, - }, - }); - - if (!existing) { - return message.reply("❌ No encontré un embed con ese nombre."); - } - - // 📌 Estado inicial desde DB - let embedState: { - title?: string; - description?: string; - color?: number; - footer?: string; - } = { - title: existing.title ?? undefined, - description: existing.description ?? undefined, - color: existing.color ? parseInt(existing.color.replace("#", ""), 16) : 0x5865f2, - footer: existing.footerText ?? undefined, - }; - - // 📌 Función para renderizar preview - const renderPreview = async () => { - const preview = new EmbedBuilder().setColor(embedState.color ?? 0x5865f2); - - if (embedState.title) - //@ts-ignore - preview.setTitle(await replaceVars(embedState.title, message.member)); - if (embedState.description) - //@ts-ignore - preview.setDescription(await replaceVars(embedState.description, message.member)); - if (embedState.footer) - preview.setFooter({ - //@ts-ignore - text: await replaceVars(embedState.footer, message.member), - }); - - return preview; - }; - - const generateButtonRows = (disabled = false) => { - const primaryRow = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("edit_title") - .setLabel("Título") - .setStyle(ButtonStyle.Primary) - .setDisabled(disabled), - new ButtonBuilder() - .setCustomId("edit_description") - .setLabel("Descripción") - .setStyle(ButtonStyle.Primary) - .setDisabled(disabled), - new ButtonBuilder() - .setCustomId("edit_color") - .setLabel("Color") - .setStyle(ButtonStyle.Primary) - .setDisabled(disabled) - ); - - const secondaryRow = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("edit_footer") - .setLabel("Footer") - .setStyle(ButtonStyle.Secondary) - .setDisabled(disabled) - ); - - const controlRow = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("save_embed") - .setLabel("Guardar cambios") - .setStyle(ButtonStyle.Success) - .setDisabled(disabled), - new ButtonBuilder() - .setCustomId("cancel_embed") - .setLabel("Cancelar") - .setStyle(ButtonStyle.Danger) - .setDisabled(disabled) - ); - - return [primaryRow, secondaryRow, controlRow]; - }; - - if (message.channel.type === ChannelType.GuildText) { - const channel = message.channel as TextChannel; - - const editorMessage = await channel.send({ - embeds: [await renderPreview()], - components: generateButtonRows(), - }); - - const collector = editorMessage.createMessageComponentCollector({ - componentType: ComponentType.Button, - time: 300000, - }); - - collector.on("collect", async (i) => { - if (i.user.id !== message.author.id) { - await i.reply({ - content: "No puedes usar este menú.", - ephemeral: true, - }); - return; - } - await i.deferUpdate(); - await editorMessage.edit({ components: generateButtonRows(true) }); - - // Guardar cambios - if (i.customId === "save_embed") { - try { - const dataForDb = { - title: embedState.title, - description: embedState.description, - color: embedState.color ? `#${embedState.color.toString(16).padStart(6, '0')}` : null, - footerText: embedState.footer, - }; - - await client.prisma.embedConfig.update({ - where: { - guildId_name: { - guildId: message.guildId!, - name: embedName, - }, - }, - data: dataForDb, - }); - - const saved = new EmbedBuilder() - .setColor(0x00ff00) - .setTitle(`✅ Actualizado: ${embedName}`) - .setDescription("Los cambios fueron guardados en la base de datos."); - - await editorMessage.edit({ - embeds: [saved], - components: [], - }); - } catch (e) { - const errorEmbed = new EmbedBuilder() - .setColor(0xff0000) - .setTitle("❌ Error al Guardar") - .setDescription("No se pudo guardar en la base de datos. Revisa la consola."); - await editorMessage.edit({ - embeds: [errorEmbed], - components: [], - }); - console.error("Error de Prisma al actualizar el embed:", e); - } - collector.stop(); - return; - } - - // Cancelar - if (i.customId === "cancel_embed") { - await editorMessage.delete(); - collector.stop(); - return; - } - - // Edición - let promptContent = ""; - let fieldToEdit: "title" | "description" | "color" | "footer" | null = - null; - - switch (i.customId) { - case "edit_title": - promptContent = "Escribe el nuevo **título** (puedes usar variables)."; - fieldToEdit = "title"; - break; - case "edit_description": - promptContent = "Escribe la nueva **descripción**."; - fieldToEdit = "description"; - break; - case "edit_color": - promptContent = "Escribe el nuevo **color** en formato hexadecimal (ej: `#FF0000`)."; - fieldToEdit = "color"; - break; - case "edit_footer": - promptContent = "Escribe el nuevo **texto del footer**."; - fieldToEdit = "footer"; - break; - } - - //@ts-ignore - const promptMessage = await i.channel.send(promptContent); - - //@ts-ignore - const messageCollector = i.channel!.createMessageCollector({ - //@ts-ignore - filter: (m: Message) => m.author.id === i.user.id, - max: 1, - time: 60000, - }); - - //@ts-ignore - messageCollector.on("collect", async (collectedMessage) => { - const newValue = collectedMessage.content; - - if (fieldToEdit === "title") embedState.title = newValue; - if (fieldToEdit === "description") embedState.description = newValue; - if (fieldToEdit === "footer") embedState.footer = newValue; - - if (fieldToEdit === "color") { - try { - const hex = newValue.replace("#", ""); - embedState.color = parseInt(hex, 16); - } catch { - embedState.color = 0x5865f2; - } - } - - await collectedMessage.delete(); - await promptMessage.delete(); - - await editorMessage.edit({ - embeds: [await renderPreview()], - components: generateButtonRows(false), - }); - }); - - //@ts-ignore - messageCollector.on("end", async (collected) => { - if (collected.size === 0) { - await promptMessage.delete(); - await editorMessage.edit({ - components: generateButtonRows(false), - }); - } - }); - }); - - collector.on("end", async (_, reason) => { - if (reason === "time") { - const timeoutEmbed = new EmbedBuilder() - .setColor(0xff0000) - .setTitle("Editor finalizado por inactividad."); - - await editorMessage.edit({ - embeds: [timeoutEmbed], - components: [], - }); - } - }); - } - }, -}; diff --git a/src/.backup/game/_helpers.ts b/src/.backup/game/_helpers.ts deleted file mode 100644 index d02a12d..0000000 --- a/src/.backup/game/_helpers.ts +++ /dev/null @@ -1,665 +0,0 @@ -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 { - Message, - TextBasedChannel, - MessageComponentInteraction, - StringSelectMenuInteraction, - ButtonInteraction, - ModalSubmitInteraction, -} from "discord.js"; -import { MessageFlags } from "discord.js"; -import { - ButtonStyle, - ComponentType, - TextInputStyle, -} from "discord-api-types/v10"; - -export function parseItemProps(json: unknown): ItemProps { - if (!json || typeof json !== "object") return {}; - return json as ItemProps; -} - -export async function resolveArea(guildId: string, areaKey: string) { - const area = await prisma.gameArea.findFirst({ - where: { key: areaKey, OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); - return area; -} - -export interface ResolvedAreaInfo { - area: GameArea | null; - source: "guild" | "global" | "none"; -} - -export async function resolveGuildAreaWithFallback( - guildId: string, - areaKey: string -): Promise { - const guildArea = await prisma.gameArea.findFirst({ - where: { key: areaKey, guildId }, - }); - if (guildArea) { - return { area: guildArea, source: "guild" }; - } - - const globalArea = await prisma.gameArea.findFirst({ - where: { key: areaKey, guildId: null }, - }); - if (globalArea) { - return { area: globalArea, source: "global" }; - } - - return { area: null, source: "none" }; -} - -export async function resolveAreaByType( - guildId: string, - type: string -): Promise { - const guildArea = await prisma.gameArea.findFirst({ - where: { type, guildId }, - orderBy: [{ createdAt: "asc" }], - }); - if (guildArea) { - return { area: guildArea, source: "guild" }; - } - - const globalArea = await prisma.gameArea.findFirst({ - where: { type, guildId: null }, - orderBy: [{ createdAt: "asc" }], - }); - if (globalArea) { - return { area: globalArea, source: "global" }; - } - - return { area: null, source: "none" }; -} - -export async function getDefaultLevel( - userId: string, - guildId: string, - areaId: string -): Promise { - const prog = await prisma.playerProgress.findUnique({ - where: { userId_guildId_areaId: { userId, guildId, areaId } }, - }); - return Math.max(1, prog?.highestLevel ?? 1); -} - -export async function findBestToolKey( - userId: string, - guildId: string, - toolType: string -): Promise { - const inv = await prisma.inventoryEntry.findMany({ - where: { userId, guildId, quantity: { gt: 0 } }, - include: { item: true }, - }); - let best: { key: string; tier: number } | null = null; - for (const e of inv) { - const it = e.item; - const props = parseItemProps(it.props); - const t = props.tool; - if (!t || t.type !== toolType) continue; - const tier = Math.max(0, t.tier ?? 0); - if (!best || tier > best.tier) best = { key: it.key, tier }; - } - return best?.key ?? null; -} - -export interface ParsedGameArgs { - levelArg: number | null; - providedTool: string | null; - areaOverride: string | null; -} - -const AREA_OVERRIDE_PREFIX = "area:"; - -export function parseGameArgs(args: string[]): ParsedGameArgs { - const tokens = args.filter( - (arg): arg is string => typeof arg === "string" && arg.trim().length > 0 - ); - - let levelArg: number | null = null; - let providedTool: string | null = null; - let areaOverride: string | null = null; - - for (const token of tokens) { - if (token.startsWith(AREA_OVERRIDE_PREFIX)) { - const override = token.slice(AREA_OVERRIDE_PREFIX.length).trim(); - if (override) areaOverride = override; - continue; - } - - if (levelArg === null && /^\d+$/.test(token)) { - levelArg = parseInt(token, 10); - continue; - } - - if (!providedTool) { - providedTool = token; - } - } - - return { levelArg, providedTool, areaOverride }; -} - -const DEFAULT_ITEM_ICON = "📦"; - -export function resolveItemIcon( - icon?: string | null, - fallback = DEFAULT_ITEM_ICON -) { - const trimmed = icon?.trim(); - return trimmed && trimmed.length > 0 ? trimmed : fallback; -} - -export function formatItemLabel( - item: { key: string; name?: string | null; icon?: string | null }, - options: { fallbackIcon?: string; bold?: boolean } = {} -): string { - const fallbackIcon = options.fallbackIcon ?? DEFAULT_ITEM_ICON; - const icon = resolveItemIcon(item.icon, fallbackIcon); - const label = (item.name ?? "").trim() || item.key; - const content = `${icon ? `${icon} ` : ""}${label}`.trim(); - return options.bold ? `**${content}**` : content; -} - -export type ItemBasicInfo = { - key: string; - name: string | null; - icon: string | null; -}; - -export async function fetchItemBasics( - guildId: string, - keys: string[] -): Promise> { - const uniqueKeys = Array.from( - new Set(keys.filter((key): key is string => Boolean(key && key.trim()))) - ); - if (uniqueKeys.length === 0) return new Map(); - - const rows = await prisma.economyItem.findMany({ - where: { - key: { in: uniqueKeys }, - OR: [{ guildId }, { guildId: null }], - }, - orderBy: [{ key: "asc" }, { guildId: "desc" }], - select: { key: true, name: true, icon: true, guildId: true }, - }); - - const result = new Map(); - for (const row of rows) { - const current = result.get(row.key); - if (!current || row.guildId === guildId) { - result.set(row.key, { key: row.key, name: row.name, icon: row.icon }); - } - } - - for (const key of uniqueKeys) { - if (!result.has(key)) { - result.set(key, { key, name: null, icon: null }); - } - } - - return result; -} - -export type AreaMetadata = - | { - previewImage?: string; - image?: string; - referenceImage?: string; - description?: string; - [k: string]: any; - } - | null - | undefined; - -export function buildAreaMetadataBlocks( - area: Pick -) { - const blocks: any[] = []; - const meta = (area.metadata as AreaMetadata) || undefined; - if (!meta) return blocks; - - const img = meta.previewImage || meta.image || meta.referenceImage; - const desc = - typeof meta.description === "string" && meta.description.trim().length > 0 - ? meta.description.trim() - : null; - - if (desc) { - blocks.push(textBlock(`**🗺️ Detalles del área**\n${desc}`)); - } - if (img && typeof img === "string") { - // Mostrar también como texto para compatibilidad, y dejar que el renderer agregue imagen si soporta - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - blocks.push(textBlock(`**🖼️ Mapa/Imagen:** ${img}`)); - // Si el renderer soporta bloque de imagen, los consumidores podrán usarlo - // @ts-ignore: el builder acepta bloques extendidos - blocks.push({ kind: "image", url: img }); - } - return blocks; -} - -export interface KeyPickerOption { - value: string; - label: string; - description?: string; - keywords?: string[]; -} - -export interface KeyPickerConfig { - entries: T[]; - getOption: (entry: T) => KeyPickerOption; - title: string; - customIdPrefix: string; - emptyText: string; - placeholder?: string; - filterHint?: string; - accentColor?: number; - userId?: string; -} - -export interface KeyPickerResult { - entry: T | null; - panelMessage: Message | null; - reason: "selected" | "empty" | "cancelled" | "timeout"; - // When present, the raw value selected from the select menu (may be id or key) - selectedValue?: string; -} - -export async function promptKeySelection( - message: Message, - config: KeyPickerConfig -): Promise> { - const channel = message.channel as TextBasedChannel & { send: Function }; - const userId = config.userId ?? message.author?.id ?? message.member?.user.id; - - const baseOptions = config.entries.map((entry) => { - const option = config.getOption(entry); - const searchText = [ - option.label, - option.description, - option.value, - ...(option.keywords ?? []), - ] - .filter(Boolean) - .join(" ") - .toLowerCase(); - return { entry, option, searchText }; - }); - - if (baseOptions.length === 0) { - const emptyPanel = { - type: 17, - accent_color: 0xffa500, - components: [ - { - type: 10, - content: config.emptyText, - }, - ], - }; - await (channel.send as any)({ - content: null, - flags: 32768, - reply: { messageReference: message.id }, - components: [emptyPanel], - }); - return { entry: null, panelMessage: null, reason: "empty" }; - } - - let filter = ""; - let page = 0; - const pageSize = 25; - const accentColor = config.accentColor ?? 0x5865f2; - const placeholder = config.placeholder ?? "Selecciona una opción…"; - - const buildComponents = () => { - const normalizedFilter = filter.trim().toLowerCase(); - const filtered = normalizedFilter - ? baseOptions.filter((item) => item.searchText.includes(normalizedFilter)) - : baseOptions; - const totalFiltered = filtered.length; - const totalPages = Math.max(1, Math.ceil(totalFiltered / pageSize)); - const safePage = Math.min(Math.max(0, page), totalPages - 1); - if (safePage !== page) page = safePage; - const start = safePage * pageSize; - const slice = filtered.slice(start, start + pageSize); - - const pageLabel = `Página ${ - totalFiltered === 0 ? 0 : safePage + 1 - }/${totalPages}`; - const statsLine = `Total: **${baseOptions.length}** • Coincidencias: **${totalFiltered}**\n${pageLabel}`; - const filterLine = filter ? `\nFiltro activo: \`${filter}\`` : ""; - const hintLine = config.filterHint ? `\n${config.filterHint}` : ""; - - const display = { - type: 17, - accent_color: accentColor, - components: [ - { type: 10, content: `# ${config.title}` }, - { type: 14, divider: true }, - { - type: 10, - content: `${statsLine}${filterLine}${hintLine}`, - }, - { type: 14, divider: true }, - { - type: 10, - content: - totalFiltered === 0 - ? "No hay resultados para el filtro actual. Ajusta el filtro o limpia la búsqueda." - : "Selecciona una opción del menú desplegable para continuar.", - }, - ], - }; - - let options = slice.map(({ option }) => ({ - label: option.label.slice(0, 100), - value: option.value, - description: option.description?.slice(0, 100), - })); - - const selectDisabled = options.length === 0; - if (selectDisabled) { - options = [ - { - label: "Sin resultados", - value: `${config.customIdPrefix}_empty`, - description: "Ajusta el filtro para ver opciones.", - }, - ]; - } - - const selectRow = { - type: 1, - components: [ - { - type: 3, - custom_id: `${config.customIdPrefix}_select`, - placeholder, - options, - disabled: selectDisabled, - }, - ], - }; - - const navRow = { - type: 1, - components: [ - { - type: 2, - style: ButtonStyle.Secondary, - label: "◀️", - custom_id: `${config.customIdPrefix}_prev`, - disabled: safePage <= 0 || totalFiltered === 0, - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "▶️", - custom_id: `${config.customIdPrefix}_next`, - disabled: safePage >= totalPages - 1 || totalFiltered === 0, - }, - { - type: 2, - style: ButtonStyle.Primary, - label: "🔎 Filtro", - custom_id: `${config.customIdPrefix}_filter`, - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Limpiar", - custom_id: `${config.customIdPrefix}_clear`, - disabled: filter.length === 0, - }, - { - type: 2, - style: ButtonStyle.Danger, - label: "Cancelar", - custom_id: `${config.customIdPrefix}_cancel`, - }, - ], - }; - - return [display, selectRow, navRow]; - }; - - const panelMessage: Message = await (channel.send as any)({ - content: null, - flags: 32768, - reply: { messageReference: message.id }, - components: buildComponents(), - }); - - let resolved = false; - - const result = await new Promise>((resolve) => { - const finish = ( - entry: T | null, - reason: "selected" | "cancelled" | "timeout", - selectedValue?: string - ) => { - if (resolved) return; - resolved = true; - resolve({ entry, panelMessage, reason, selectedValue }); - }; - - const collector = panelMessage.createMessageComponentCollector({ - time: 5 * 60_000, - filter: (i: MessageComponentInteraction) => - i.user.id === userId && i.customId.startsWith(config.customIdPrefix), - }); - - collector.on( - "collect", - async (interaction: MessageComponentInteraction) => { - try { - if ( - interaction.customId === `${config.customIdPrefix}_select` && - interaction.isStringSelectMenu() - ) { - const select = interaction as StringSelectMenuInteraction; - const value = select.values?.[0]; - const selected = baseOptions.find( - (opt) => opt.option.value === value - ); - if (!selected) { - await select.reply({ - content: "❌ Opción no válida.", - flags: MessageFlags.Ephemeral, - }); - return; - } - - try { - await select.update({ - components: [ - { - type: 17, - accent_color: accentColor, - components: [ - { - type: 10, - content: `⏳ Cargando **${selected.option.label}**…`, - }, - ], - }, - ], - }); - } catch { - if (!select.deferred && !select.replied) { - try { - await select.deferUpdate(); - } catch {} - } - } - - finish(selected.entry, "selected", value); - collector.stop("selected"); - return; - } - - if ( - interaction.customId === `${config.customIdPrefix}_prev` && - interaction.isButton() - ) { - if (page > 0) page -= 1; - await interaction.update({ components: buildComponents() }); - return; - } - - if ( - interaction.customId === `${config.customIdPrefix}_next` && - interaction.isButton() - ) { - page += 1; - await interaction.update({ components: buildComponents() }); - return; - } - - if ( - interaction.customId === `${config.customIdPrefix}_clear` && - interaction.isButton() - ) { - filter = ""; - page = 0; - await interaction.update({ components: buildComponents() }); - return; - } - - if ( - interaction.customId === `${config.customIdPrefix}_cancel` && - interaction.isButton() - ) { - try { - await interaction.update({ - components: [ - { - type: 17, - accent_color: 0xff0000, - components: [ - { type: 10, content: "❌ Selección cancelada." }, - ], - }, - ], - }); - } catch { - if (!interaction.deferred && !interaction.replied) { - try { - await interaction.deferUpdate(); - } catch {} - } - } - - finish(null, "cancelled"); - collector.stop("cancelled"); - return; - } - - if ( - interaction.customId === `${config.customIdPrefix}_filter` && - interaction.isButton() - ) { - const modal = { - title: "Filtrar lista", - customId: `${config.customIdPrefix}_filter_modal`, - components: [ - { - type: ComponentType.Label, - label: "Texto a buscar", - component: { - type: ComponentType.TextInput, - customId: "query", - style: TextInputStyle.Short, - required: false, - value: filter, - placeholder: "Nombre, key, categoría…", - }, - }, - ], - } as const; - - await (interaction as ButtonInteraction).showModal(modal); - let submitted: ModalSubmitInteraction | undefined; - try { - submitted = await interaction.awaitModalSubmit({ - time: 120_000, - filter: (sub) => - sub.user.id === userId && - sub.customId === `${config.customIdPrefix}_filter_modal`, - }); - } catch { - return; - } - - try { - const value = - submitted.components.getTextInputValue("query")?.trim() ?? ""; - filter = value; - page = 0; - await submitted.deferUpdate(); - await panelMessage.edit({ components: buildComponents() }); - } catch { - // ignore errors updating filter - } - return; - } - } catch (err) { - if (!interaction.deferred && !interaction.replied) { - await interaction.reply({ - content: "❌ Error procesando la selección.", - flags: MessageFlags.Ephemeral, - }); - } - } - } - ); - - collector.on("end", async (_collected, reason) => { - if (resolved) return; - resolved = true; - if (reason !== "selected" && reason !== "cancelled") { - const expiredPanel = { - type: 17, - accent_color: 0xffa500, - components: [{ type: 10, content: "⏰ Selección expirada." }], - }; - try { - await panelMessage.edit({ components: [expiredPanel] }); - } catch {} - } - - let mappedReason: "selected" | "cancelled" | "timeout"; - if (reason === "selected") mappedReason = "selected"; - else if (reason === "cancelled") mappedReason = "cancelled"; - else mappedReason = "timeout"; - - resolve({ entry: null, panelMessage, reason: mappedReason }); - }); - }); - - return result; -} - -export function sendDisplayReply( - message: Message, - display: any, - extraComponents: any[] = [] -) { - const channel = message.channel as TextBasedChannel & { send: Function }; - return (channel.send as any)({ - flags: 32768, - message_reference: { message_id: message.id }, - components: [display, ...extraComponents], - }); -} diff --git a/src/.backup/game/abrir.ts b/src/.backup/game/abrir.ts deleted file mode 100644 index 13ab271..0000000 --- a/src/.backup/game/abrir.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { openChestByKey } from '../../../game/economy/service'; -import { prisma } from '../../../core/database/prisma'; -import { fetchItemBasics, formatItemLabel } from './_helpers'; -import type { ItemBasicInfo } from './_helpers'; - -export const command: CommandMessage = { - name: 'abrir', - type: 'message', - aliases: ['open'], - cooldown: 3, - description: 'Abre un cofre (item) por key y recibe sus recompensas/roles.', - usage: 'abrir ', - run: async (message, args, _client: Amayo) => { - const itemKey = args[0]?.trim(); - if (!itemKey) { await message.reply('Uso: `!abrir `'); return; } - const userId = message.author.id; - const guildId = message.guild!.id; - try { - const res = await openChestByKey(userId, guildId, itemKey); - - const keyRewards = res.itemsToAdd - .map((it) => it.itemKey) - .filter((key): key is string => typeof key === 'string' && key.trim().length > 0); - - const basicsKeys = [itemKey, ...keyRewards].filter((key): key is string => typeof key === 'string' && key.trim().length > 0); - const infoMap = basicsKeys.length > 0 ? await fetchItemBasics(guildId, basicsKeys) : new Map(); - - const idRewards = res.itemsToAdd - .filter((it) => !it.itemKey && it.itemId) - .map((it) => it.itemId!) - .filter((id, idx, arr) => arr.indexOf(id) === idx); - const itemsById = new Map(); - if (idRewards.length) { - const rows = await prisma.economyItem.findMany({ - where: { id: { in: idRewards } }, - select: { id: true, key: true, name: true, icon: true }, - }); - for (const row of rows) { - const info = { key: row.key, name: row.name, icon: row.icon }; - itemsById.set(row.id, info); - if (!infoMap.has(row.key)) infoMap.set(row.key, info); - } - } - - const chestLabel = formatItemLabel(infoMap.get(itemKey) ?? { key: itemKey, name: null, icon: null }, { bold: true }); - const coins = res.coinsDelta ? `🪙 +${res.coinsDelta}` : ''; - const items = res.itemsToAdd.length - ? res.itemsToAdd.map((i) => { - const info = i.itemKey - ? infoMap.get(i.itemKey) - : i.itemId - ? itemsById.get(i.itemId) - : null; - const label = info - ? formatItemLabel(info) - : formatItemLabel({ key: i.itemKey ?? (i.itemId ?? 'item'), name: null, icon: null }); - return `${label} x${i.qty}`; - }).join(' · ') - : ''; - let rolesGiven: string[] = []; - let rolesFailed: string[] = []; - if (res.rolesToGrant.length && message.member) { - for (const r of res.rolesToGrant) { - try { await message.member.roles.add(r); rolesGiven.push(r); } catch { rolesFailed.push(r); } - } - } - const lines = [ - `🎁 Abriste ${chestLabel}${res.consumed ? ' (consumido 1)' : ''}`, - coins && `Monedas: ${coins}`, - items && `Ítems: ${items}`, - rolesGiven.length ? `Roles otorgados: ${rolesGiven.map(id=>`<@&${id}>`).join(', ')}` : '', - rolesFailed.length ? `Roles fallidos: ${rolesFailed.join(', ')}` : '', - ].filter(Boolean); - await message.reply(lines.join('\n')); - } catch (e: any) { - await message.reply(`❌ No se pudo abrir ${itemKey}: ${e?.message ?? e}`); - } - } -}; - diff --git a/src/.backup/game/areaCreate.ts b/src/.backup/game/areaCreate.ts deleted file mode 100644 index 563f4ad..0000000 --- a/src/.backup/game/areaCreate.ts +++ /dev/null @@ -1,463 +0,0 @@ -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, - TextBasedChannel, -} from "discord.js"; -import { - ComponentType, - TextInputStyle, - ButtonStyle, -} from "discord-api-types/v10"; - -interface AreaState { - key: string; - name?: string; - type?: string; - config?: any; - metadata?: any; -} - -function buildAreaDisplay(state: AreaState, editing: boolean = false) { - const title = editing ? "Editando Área" : "Creando Área"; - const statusText = [ - "**📋 Estado Actual:**", - `**Nombre:** ${state.name || "❌ No configurado"}`, - `**Tipo:** ${state.type || "❌ No configurado"}`, - `**Config:** ${Object.keys(state.config || {}).length} campos`, - `**Metadata:** ${Object.keys(state.metadata || {}).length} campos`, - ].join("\n"); - - const instructionsText = [ - "**🎮 Instrucciones:**", - "• **Base**: Configura nombre y tipo", - "• **Config (JSON)**: Configuración técnica", - "• **Meta (JSON)**: Metadatos adicionales", - "• **Guardar**: Confirma los cambios", - "• **Cancelar**: Descarta los cambios", - ].join("\n"); - - return { - type: 17, - accent_color: 0x00ff00, - components: [ - { - type: 10, - content: `# 🗺️ ${title}: \`${state.key}\``, - }, - { type: 14, divider: true }, - { - type: 10, - content: statusText, - }, - { type: 14, divider: true }, - { - type: 10, - content: instructionsText, - }, - ], - }; -} - -const buildEditorComponents = (state: AreaState, editing: boolean = false) => [ - buildAreaDisplay(state, editing), - { - type: 1, - components: [ - { - type: 2, - style: ButtonStyle.Primary, - label: "Base", - custom_id: "ga_base", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Config (JSON)", - custom_id: "ga_config", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Meta (JSON)", - custom_id: "ga_meta", - }, - { - type: 2, - style: ButtonStyle.Success, - label: "Guardar", - custom_id: "ga_save", - }, - { - type: 2, - style: ButtonStyle.Danger, - label: "Cancelar", - custom_id: "ga_cancel", - }, - ], - }, -]; - -export const command: CommandMessage = { - name: "area-crear", - type: "message", - aliases: ["crear-area", "areacreate"], - cooldown: 10, - description: - "Crea una GameArea (mina/laguna/arena/farm) para este servidor con editor.", - usage: "area-crear ", - run: async (message, args, _client: Amayo) => { - const channel = message.channel as TextBasedChannel & { send: Function }; - const allowed = await hasManageGuildOrStaff( - message.member, - message.guild!.id, - prisma - ); - if (!allowed) { - await (channel.send as any)({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xff0000, - components: [ - { - type: 10, - content: - "❌ **Error de Permisos**\n└ No tienes permisos de ManageGuild ni rol de staff.", - }, - ], - }, - ], - reply: { messageReference: message.id }, - }); - return; - } - - const key = args[0]?.trim(); - if (!key) { - await (channel.send as any)({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xffa500, - components: [ - { - type: 10, - content: - "⚠️ **Uso Incorrecto**\n└ Uso: `!area-crear `", - }, - ], - }, - ], - reply: { messageReference: message.id }, - }); - return; - } - - const guildId = message.guild!.id; - const exists = await prisma.gameArea.findFirst({ where: { key, guildId } }); - if (exists) { - await (channel.send as any)({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xff0000, - components: [ - { - type: 10, - content: - "❌ **Área Ya Existe**\n└ Ya existe un área con esa key en este servidor.", - }, - ], - }, - ], - reply: { messageReference: message.id }, - }); - return; - } - - const state: AreaState = { key, config: {}, metadata: {} }; - - const editorMsg = await (channel.send as any)({ - content: null, - flags: 32768, - components: buildEditorComponents(state, false), - reply: { messageReference: message.id }, - }); - - const collector = editorMsg.createMessageComponentCollector({ - time: 30 * 60_000, - filter: (i) => i.user.id === message.author.id, - }); - collector.on("collect", async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - switch (i.customId) { - case "ga_cancel": - await i.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xff0000, - components: [ - { - type: 10, - content: "**❌ Editor de Área cancelado.**", - }, - ], - }, - ], - }); - collector.stop("cancel"); - return; - case "ga_base": - await showBaseModal( - i as ButtonInteraction, - state, - editorMsg, - false - ); - return; - case "ga_config": - await showJsonModal( - i as ButtonInteraction, - state, - "config", - "Config del Área", - editorMsg, - false - ); - return; - case "ga_meta": - await showJsonModal( - i as ButtonInteraction, - state, - "metadata", - "Meta del Área", - editorMsg, - false - ); - return; - case "ga_save": - if (!state.name || !state.type) { - await i.reply({ - content: "❌ Completa Base (nombre/tipo).", - flags: MessageFlags.Ephemeral, - }); - return; - } - await prisma.gameArea.create({ - data: { - guildId, - key: state.key, - name: state.name!, - type: state.type!, - config: state.config ?? {}, - metadata: state.metadata ?? {}, - }, - }); - await i.reply({ - content: "✅ Área guardada.", - flags: MessageFlags.Ephemeral, - }); - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0x00ff00, - components: [ - { - type: 10, - content: `**✅ Área \`${state.key}\` creada exitosamente.**`, - }, - ], - }, - ], - }); - collector.stop("saved"); - return; - } - } catch (e) { - if (!i.deferred && !i.replied) - await i.reply({ - content: "❌ Error procesando la acción.", - flags: MessageFlags.Ephemeral, - }); - } - }); - - collector.on("end", async (_c, r) => { - if (r === "time") { - try { - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xffa500, - components: [ - { - type: 10, - content: "**⏰ Editor expirado.**", - }, - ], - }, - ], - }); - } catch {} - } - }); - }, -}; - -async function showBaseModal( - i: ButtonInteraction, - state: AreaState, - editorMsg: Message, - editing: boolean -) { - const modal = { - title: "Base del Área", - customId: "ga_base_modal", - components: [ - { - type: ComponentType.Label, - label: "Nombre", - component: { - type: ComponentType.TextInput, - customId: "name", - style: TextInputStyle.Short, - required: true, - value: state.name ?? "", - }, - }, - { - type: ComponentType.Label, - label: "Tipo (MINE/LAGOON/FIGHT/FARM)", - component: { - type: ComponentType.TextInput, - customId: "type", - style: TextInputStyle.Short, - required: true, - value: state.type ?? "", - }, - }, - { - type: ComponentType.Label, - label: "Imagen de referencia (URL, opcional)", - component: { - type: ComponentType.TextInput, - customId: "referenceImage", - style: TextInputStyle.Short, - required: false, - value: - (state.metadata && - (state.metadata.referenceImage || - state.metadata.image || - state.metadata.previewImage)) ?? - "", - }, - }, - ], - } as const; - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - state.name = sub.components.getTextInputValue("name").trim(); - state.type = sub.components.getTextInputValue("type").trim().toUpperCase(); - try { - const ref = sub.components.getTextInputValue("referenceImage")?.trim(); - if (ref && ref.length > 0) { - state.metadata = state.metadata || {}; - // store as referenceImage for consumers; renderer looks at previewImage/image/referenceImage - (state.metadata as any).referenceImage = ref; - } - } catch {} - await sub.reply({ - content: "✅ Base actualizada.", - flags: MessageFlags.Ephemeral, - }); - - // Actualizar display - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildEditorComponents(state, editing), - }); - } catch {} -} - -async function showJsonModal( - i: ButtonInteraction, - state: AreaState, - field: "config" | "metadata", - title: string, - editorMsg: Message, - editing: boolean -) { - const current = JSON.stringify(state[field] ?? {}); - const modal = { - title, - customId: `ga_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, - }); - return; - } - } else { - state[field] = {}; - await sub.reply({ content: "ℹ️ Limpio.", flags: MessageFlags.Ephemeral }); - } - - // Actualizar display - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildEditorComponents(state, editing), - }); - } catch {} -} diff --git a/src/.backup/game/areaEdit.ts b/src/.backup/game/areaEdit.ts deleted file mode 100644 index 9b68b48..0000000 --- a/src/.backup/game/areaEdit.ts +++ /dev/null @@ -1,256 +0,0 @@ -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, TextBasedChannel } from 'discord.js'; -import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10'; -import { promptKeySelection } from './_helpers'; - -interface AreaState { - key: string; - name?: string; - type?: string; - config?: any; - metadata?: any; -} - -function buildAreaDisplay(state: AreaState, editing: boolean = false) { - const title = editing ? 'Editando Área' : 'Creando Área'; - const statusText = [ - '**📋 Estado Actual:**', - `**Nombre:** ${state.name || '❌ No configurado'}`, - `**Tipo:** ${state.type || '❌ No configurado'}`, - `**Config:** ${Object.keys(state.config || {}).length} campos`, - `**Metadata:** ${Object.keys(state.metadata || {}).length} campos` - ].join('\n'); - - const instructionsText = [ - '**🎮 Instrucciones:**', - '• **Base**: Configura nombre y tipo', - '• **Config (JSON)**: Configuración técnica', - '• **Meta (JSON)**: Metadatos adicionales', - '• **Guardar**: Confirma los cambios', - '• **Cancelar**: Descarta los cambios' - ].join('\n'); - - return { - type: 17, - accent_color: 0x00FF00, - components: [ - { - type: 10, - content: `# 🗺️ ${title}: \`${state.key}\`` - }, - { type: 14, divider: true }, - { - type: 10, - content: statusText - }, - { type: 14, divider: true }, - { - type: 10, - content: instructionsText - } - ] - }; -} - -const buildEditorComponents = (state: AreaState, editing: boolean = false) => [ - buildAreaDisplay(state, editing), - { - type: 1, - components: [ - { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'ga_base' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Config (JSON)', custom_id: 'ga_config' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'ga_meta' }, - { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'ga_save' }, - { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'ga_cancel' }, - ] - } -]; - -export const command: CommandMessage = { - name: 'area-editar', - type: 'message', - aliases: ['editar-area','areaedit'], - cooldown: 10, - description: 'Edita una GameArea de este servidor con un editor interactivo.', - usage: 'area-editar', - run: async (message, args, _client: Amayo) => { - const channel = message.channel as TextBasedChannel & { send: Function }; - const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, prisma); - if (!allowed) { - await (channel.send as any)({ - content: null, - flags: 32768, - components: [{ - type: 17, - accent_color: 0xFF0000, - components: [{ - type: 10, - content: '❌ **Error de Permisos**\n└ No tienes permisos de ManageGuild ni rol de staff.' - }] - }], - reply: { messageReference: message.id } - }); - return; - } - - const guildId = message.guild!.id; - const areas = await prisma.gameArea.findMany({ where: { guildId }, orderBy: [{ key: 'asc' }] }); - const selection = await promptKeySelection(message, { - entries: areas, - customIdPrefix: 'area_edit', - title: 'Selecciona un área para editar', - emptyText: '⚠️ **No hay áreas configuradas.** Usa `!area-crear` para crear una nueva.', - placeholder: 'Elige un área…', - filterHint: 'Puedes filtrar por nombre, key o tipo.', - getOption: (area) => ({ - value: area.id, - label: `${area.name ?? area.key} (${area.type})`, - description: area.key, - keywords: [area.key, area.name ?? '', area.type ?? ''], - }), - }); - - if (!selection.entry || !selection.panelMessage) { - return; - } - - const area = selection.entry; - const state: AreaState = { key: area.key, name: area.name, type: area.type, config: area.config ?? {}, metadata: area.metadata ?? {} }; - - const editorMsg = selection.panelMessage; - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildEditorComponents(state, true), - }); - - const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i)=> i.user.id === message.author.id }); - collector.on('collect', async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - switch (i.customId) { - case 'ga_cancel': - await i.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: [{ - type: 17, - accent_color: 0xFF0000, - components: [{ - type: 10, - content: '**❌ Editor de Área cancelado.**' - }] - }] - }); - collector.stop('cancel'); - return; - case 'ga_base': - await showBaseModal(i as ButtonInteraction, state, editorMsg, true); - return; - case 'ga_config': - await showJsonModal(i as ButtonInteraction, state, 'config', 'Config del Área', editorMsg, true); - return; - case 'ga_meta': - await showJsonModal(i as ButtonInteraction, state, 'metadata', 'Meta del Área', editorMsg, true); - return; - case 'ga_save': - if (!state.name || !state.type) { await i.reply({ content: '❌ Completa Base (nombre/tipo).', flags: MessageFlags.Ephemeral }); return; } - await prisma.gameArea.update({ where: { id: area.id }, data: { name: state.name!, type: state.type!, config: state.config ?? {}, metadata: state.metadata ?? {} } }); - await i.reply({ content: '✅ Área actualizada.', flags: MessageFlags.Ephemeral }); - await editorMsg.edit({ - content: null, - flags: 32768, - components: [{ - type: 17, - accent_color: 0x00FF00, - components: [{ - type: 10, - content: `**✅ Área \`${state.key}\` actualizada exitosamente.**` - }] - }] - }); - collector.stop('saved'); - return; - } - } catch (e) { - if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral }); - } - }); - - collector.on('end', async (_c,r)=> { - if (r==='time') { - try { - await editorMsg.edit({ - content: null, - flags: 32768, - components: [{ - type: 17, - accent_color: 0xFFA500, - components: [{ - type: 10, - content: '**⏰ Editor expirado.**' - }] - }] - }); - } catch {} - } - }); - } -}; - -async function showBaseModal(i: ButtonInteraction, state: AreaState, editorMsg: Message, editing: boolean) { - const modal = { title: 'Base del Área', customId: 'ga_base_modal', components: [ - { type: ComponentType.Label, label: 'Nombre', component: { type: ComponentType.TextInput, customId: 'name', style: TextInputStyle.Short, required: true, value: state.name ?? '' } }, - { type: ComponentType.Label, label: 'Tipo (MINE/LAGOON/FIGHT/FARM)', component: { type: ComponentType.TextInput, customId: 'type', style: TextInputStyle.Short, required: true, value: state.type ?? '' } }, - ] } as const; - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - state.name = sub.components.getTextInputValue('name').trim(); - state.type = sub.components.getTextInputValue('type').trim().toUpperCase(); - await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral }); - - // Actualizar display - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildEditorComponents(state, editing) - }); - } catch {} -} - -async function showJsonModal(i: ButtonInteraction, state: AreaState, field: 'config'|'metadata', title: string, editorMsg: Message, editing: boolean) { - const current = JSON.stringify(state[field] ?? {}); - const modal = { title, customId: `ga_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 }); - return; - } - } else { - state[field] = {}; - await sub.reply({ content: 'ℹ️ Limpio.', flags: MessageFlags.Ephemeral }); - } - - // Actualizar display - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildEditorComponents(state, editing) - }); - } catch {} -} - diff --git a/src/.backup/game/areaNivel.ts b/src/.backup/game/areaNivel.ts deleted file mode 100644 index 7b55484..0000000 --- a/src/.backup/game/areaNivel.ts +++ /dev/null @@ -1,336 +0,0 @@ -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; - level: number; - 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"], - cooldown: 10, - description: - "Crea o edita un nivel de una GameArea (requisitos, recompensas, mobs, ventana).", - usage: "area-nivel ", - 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 areaKey = args[0]?.trim(); - const levelNum = parseInt(args[1] || "", 10); - if (!areaKey || !Number.isFinite(levelNum) || levelNum <= 0) { - await message.reply("Uso: `!area-nivel `"); - 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 existing = await prisma.gameAreaLevel.findFirst({ - where: { areaId: area.id, level: levelNum }, - }); - - const state: LevelState = { - areaKey, - level: levelNum, - requirements: existing?.requirements ?? {}, - rewards: existing?.rewards ?? {}, - mobs: existing?.mobs ?? {}, - 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)" - }`, - 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: 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) => { - 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_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 ?? {}, - 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, - }); - } 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"); - return; - } - } catch (e) { - 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 {} - } - }); - }, -}; - -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; - 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 }); - } - } 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; - 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, - }); - } catch {} -} diff --git a/src/.backup/game/combatehistorial.ts b/src/.backup/game/combatehistorial.ts deleted file mode 100644 index acd7bdb..0000000 --- a/src/.backup/game/combatehistorial.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { prisma } from "../../../core/database/prisma"; -import { - buildDisplay, - dividerBlock, - textBlock, -} from "../../../core/lib/componentsV2"; -import { combatSummaryRPG } from "../../../game/lib/rpgFormat"; - -export const command: CommandMessage = { - name: "combate-historial", - type: "message", - aliases: ["fight-log", "combate-log", "battle-log"], - cooldown: 5, - description: - "Muestra tus últimos combates (resumen de daño, mobs y resultado).", - usage: "combate-historial [cantidad=5]", - run: async (message, args, _client: Amayo) => { - const userId = message.author.id; - const guildId = message.guild!.id; - const limit = Math.min(15, Math.max(1, parseInt(args[0] || "5"))); - - const runs = await prisma.minigameRun.findMany({ - where: { userId, guildId }, - orderBy: { finishedAt: "desc" }, - take: limit * 2, // tomar extra por si algunas no tienen combate - }); - - if (!runs.length) { - await message.reply("No tienes combates registrados aún."); - return; - } - - const blocks = [ - textBlock(`# 📜 Historial de Combates (${runs.length})`), - dividerBlock(), - ]; - - let added = 0; - for (const run of runs) { - const result: any = run.result as any; - const combat = result?.combat; - if (!combat) continue; - const areaId = run.areaId; - const area = await prisma.gameArea.findUnique({ where: { id: areaId } }); - const areaLabel = area ? area.name || area.key : "Área desconocida"; - const line = combatSummaryRPG({ - mobs: combat.mobs?.length || result.mobs?.length || 0, - mobsDefeated: combat.mobsDefeated || 0, - totalDamageDealt: combat.totalDamageDealt || 0, - totalDamageTaken: combat.totalDamageTaken || 0, - playerStartHp: combat.playerStartHp, - playerEndHp: combat.playerEndHp, - outcome: combat.outcome, - }); - blocks.push(textBlock(`**${areaLabel}** (Lv ${run.level})\n${line}`)); - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - added++; - if (added >= limit) break; - } - - const display = buildDisplay(0x9156ec, blocks); - await message.reply({ content: "", components: [display] }); - }, -}; diff --git a/src/.backup/game/comer.ts b/src/.backup/game/comer.ts deleted file mode 100644 index be16a83..0000000 --- a/src/.backup/game/comer.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { useConsumableByKey } from '../../../game/consumables/service'; -import { fetchItemBasics, formatItemLabel } from './_helpers'; - -export const command: CommandMessage = { - name: 'comer', - type: 'message', - aliases: ['usar-comida','usar'], - cooldown: 3, - description: 'Usa un ítem consumible (comida/poción) para curarte. Respeta cooldowns.', - usage: 'comer ', - run: async (message, args, _client: Amayo) => { - const itemKey = args[0]?.trim(); - if (!itemKey) { await message.reply('Uso: `!comer `'); return; } - const guildId = message.guild!.id; - const userId = message.author.id; - let itemInfo: { key: string; name: string | null; icon: string | null } = { key: itemKey, name: null, icon: null }; - try { - const basics = await fetchItemBasics(guildId, [itemKey]); - itemInfo = basics.get(itemKey) ?? itemInfo; - - const res = await useConsumableByKey(userId, guildId, itemKey); - const label = formatItemLabel(itemInfo, { bold: true }); - await message.reply(`🍽️ Usaste ${label}. Curado: +${res.healed} HP.`); - } catch (e: any) { - const label = formatItemLabel(itemInfo, { bold: true }); - await message.reply(`❌ No se pudo usar ${label}: ${e?.message ?? e}`); - } - } -}; - diff --git a/src/.backup/game/comprar.ts b/src/.backup/game/comprar.ts deleted file mode 100644 index ff3ea54..0000000 --- a/src/.backup/game/comprar.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { buyFromOffer } from '../../../game/economy/service'; -import { formatItemLabel } from './_helpers'; - -export const command: CommandMessage = { - name: 'comprar', - type: 'message', - aliases: ['buy'], - cooldown: 3, - description: 'Compra una oferta de la tienda por su ID. Respeta límites y stock.', - usage: 'comprar [qty] (ej: comprar off_123 2)', - run: async (message, args, _client: Amayo) => { - const offerId = args[0]?.trim(); - const qty = Math.max(1, parseInt(args[1] || '1', 10) || 1); - if (!offerId) { await message.reply('Uso: `!comprar [qty]`'); return; } - try { - const res = await buyFromOffer(message.author.id, message.guild!.id, offerId, qty); - const label = formatItemLabel(res.item, { bold: true }); - await message.reply(`🛒 Comprado: ${label} x${res.qty}`); - } catch (e: any) { - await message.reply(`❌ No se pudo comprar: ${e?.message ?? e}`); - } - } -}; - diff --git a/src/.backup/game/cooldowns.ts b/src/.backup/game/cooldowns.ts deleted file mode 100644 index 3094208..0000000 --- a/src/.backup/game/cooldowns.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { prisma } from '../../../core/database/prisma'; -import type { TextBasedChannel } from 'discord.js'; - -export const command: CommandMessage = { - name: 'cooldowns', - type: 'message', - aliases: ['cds', 'tiempos', 'cd'], - cooldown: 3, - description: 'Ver todos tus cooldowns activos', - usage: 'cooldowns', - run: async (message, args, client: Amayo) => { - try { - const userId = message.author.id; - const guildId = message.guild!.id; - - // Obtener todos los cooldowns activos - const cooldowns = await prisma.actionCooldown.findMany({ - where: { - userId, - guildId, - until: { gt: new Date() } - }, - orderBy: { until: 'asc' } - }); - - if (cooldowns.length === 0) { - await message.reply('✅ No tienes cooldowns activos. ¡Puedes realizar cualquier acción!'); - return; - } - - // Emojis por tipo de acción - const actionEmojis: Record = { - 'mine': '⛏️', - 'fish': '🎣', - 'fight': '⚔️', - 'farm': '🌾', - 'craft': '🛠️', - 'smelt': '🔥', - 'shop': '🛒', - 'daily': '🎁', - 'consume': '🍖' - }; - - // Traducción de acciones - const actionNames: Record = { - 'mine': 'Minar', - 'fish': 'Pescar', - 'fight': 'Pelear', - 'farm': 'Granja', - 'craft': 'Craftear', - 'smelt': 'Fundir', - 'shop': 'Tienda', - 'daily': 'Diario', - 'consume': 'Consumir' - }; - - let cooldownText = ''; - const now = Date.now(); - - for (const cd of cooldowns) { - const remainingMs = cd.until.getTime() - now; - const remainingSec = Math.ceil(remainingMs / 1000); - - // Formatear tiempo - let timeStr = ''; - if (remainingSec >= 3600) { - const hours = Math.floor(remainingSec / 3600); - const mins = Math.floor((remainingSec % 3600) / 60); - timeStr = `${hours}h ${mins}m`; - } else if (remainingSec >= 60) { - const mins = Math.floor(remainingSec / 60); - const secs = remainingSec % 60; - timeStr = `${mins}m ${secs}s`; - } else { - timeStr = `${remainingSec}s`; - } - - // Buscar emoji y nombre - const action = cd.key.split(':')[0]; - const emoji = actionEmojis[action] || '⏱️'; - const actionName = actionNames[action] || cd.key; - - cooldownText += `${emoji} **${actionName}**: ${timeStr}\n`; - } - - // Crear DisplayComponent - const display = { - type: 17, - accent_color: 0xFF6B6B, - components: [ - { - type: 10, - content: `# ⏰ Cooldowns Activos\n${message.author.username}, estos son tus cooldowns:` - }, - { type: 14, divider: true }, - { - type: 9, - components: [{ - type: 10, - content: `**📋 Cooldowns (${cooldowns.length})**\n${cooldownText}` - }] - }, - { type: 14, spacing: 1 }, - { - type: 10, - content: `*Los cooldowns se actualizan en tiempo real*` - } - ] - }; - - // Enviar con flags - const channel = message.channel as TextBasedChannel & { send: Function }; - await (channel.send as any)({ - display, - flags: 32768, // MessageFlags.IS_COMPONENTS_V2 - reply: { messageReference: message.id } - }); - } catch (error) { - console.error('Error en comando cooldowns:', error); - await message.reply('❌ Error al obtener los cooldowns.'); - } - } -}; diff --git a/src/.backup/game/craftear.ts b/src/.backup/game/craftear.ts deleted file mode 100644 index ce3d25e..0000000 --- a/src/.backup/game/craftear.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { craftByProductKey } from '../../../game/economy/service'; -import { fetchItemBasics, formatItemLabel } from './_helpers'; - -export const command: CommandMessage = { - name: 'craftear', - type: 'message', - aliases: ['craft'], - cooldown: 3, - description: 'Craftea un ítem por su productKey, consumiendo ingredientes según la receta.', - usage: 'craftear [veces] (ej: craftear ingot.iron 3)', - run: async (message, args, _client: Amayo) => { - const productKey = args[0]?.trim(); - const times = Math.max(1, parseInt(args[1] || '1', 10) || 1); - if (!productKey) { await message.reply('Uso: `!craftear [veces]`'); return; } - - const guildId = message.guild!.id; - const userId = message.author.id; - let itemInfo: { key: string; name: string | null; icon: string | null } = { key: productKey, name: null, icon: null }; - try { - const basics = await fetchItemBasics(guildId, [productKey]); - itemInfo = basics.get(productKey) ?? itemInfo; - } catch (err) { - console.error('No se pudo resolver info de item para craftear', err); - } - - let crafted = 0; - let lastError: any = null; - for (let i = 0; i < times; i++) { - try { - const res = await craftByProductKey(userId, guildId, productKey); - crafted += res.added; - itemInfo = { key: res.product.key, name: res.product.name, icon: res.product.icon }; - } catch (e: any) { - lastError = e; break; - } - } - - const label = formatItemLabel(itemInfo, { bold: true }); - if (crafted > 0) { - await message.reply(`🛠️ Crafteado ${label} x${crafted}.`); - } else { - await message.reply(`❌ No se pudo craftear ${label}: ${lastError?.message ?? 'revise ingredientes/receta'}`); - } - } -}; - diff --git a/src/.backup/game/deathlog.ts b/src/.backup/game/deathlog.ts deleted file mode 100644 index d007464..0000000 --- a/src/.backup/game/deathlog.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { prisma } from "../../../core/database/prisma"; - -export const command: CommandMessage = { - name: "deathlog", - aliases: ["muertes"], - type: "message", - cooldown: 8, - category: "Economía", - description: "Muestra tus últimas muertes y penalizaciones aplicadas.", - usage: "deathlog [cantidad<=20]", - run: async (message, args, _client: Amayo) => { - const userId = message.author.id; - const guildId = message.guild!.id; - let take = 10; - if (args[0]) { - const n = parseInt(args[0], 10); - if (!isNaN(n) && n > 0) take = Math.min(20, n); - } - - const logs = await prisma.deathLog.findMany({ - where: { userId, guildId }, - orderBy: { createdAt: "desc" }, - take, - }); - if (!logs.length) { - await message.reply("No hay registros de muerte."); - return; - } - - const lines = logs.map((l) => { - const pct = Math.round((l.percentApplied || 0) * 100); - const parts: string[] = []; - parts.push(`💰-${l.goldLost}`); - if (pct) parts.push(`${pct}%`); - if (l.fatigueMagnitude) - parts.push(`Fatiga ${Math.round(l.fatigueMagnitude * 100)}%`); - const area = l.areaKey ? l.areaKey : "?"; - return `${l.createdAt.toISOString().slice(11, 19)} | ${area} L${ - l.level ?? "-" - } | ${parts.join(" | ")}${l.autoDefeatNoWeapon ? " | sin arma" : ""}`; - }); - - await message.reply( - `**DeathLog (últimos ${logs.length})**\n${lines.join("\n")}` - ); - }, -}; diff --git a/src/.backup/game/durabilidad.ts b/src/.backup/game/durabilidad.ts deleted file mode 100644 index a35b72a..0000000 --- a/src/.backup/game/durabilidad.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { prisma } from "../../../core/database/prisma"; - -type ItemProps = { - tool?: { type: string; tier?: number }; - damage?: number; - defense?: number; - maxHpBonus?: number; - breakable?: { - enabled?: boolean; - maxDurability?: number; - durabilityPerUse?: number; - }; - [k: string]: unknown; -}; - -type InventoryState = { - instances?: Array<{ - durability?: number; - [k: string]: unknown; - }>; - [k: string]: unknown; -}; - -export const command: CommandMessage = { - name: "durabilidad", - type: "message", - aliases: ["dur", "durability"], - cooldown: 3, - category: "Juegos", - description: - "Muestra la durabilidad de tus items no-apilables (herramientas, armas, armaduras).", - usage: "durabilidad", - run: async (message, args, _client: Amayo) => { - const userId = message.author.id; - const guildId = message.guild!.id; - - const entries = await prisma.inventoryEntry.findMany({ - where: { userId, guildId }, - include: { item: true }, - }); - - const durableItems = entries.filter((e) => { - const props = e.item.props as ItemProps; - return ( - !e.item.stackable && - props.breakable && - props.breakable.enabled !== false - ); - }); - - if (durableItems.length === 0) { - await message.reply( - "📦 No tienes items con durabilidad en tu inventario." - ); - return; - } - - let output = `🔧 **Durabilidad de Items**\n\n`; - - for (const entry of durableItems) { - const item = entry.item; - const props = item.props as ItemProps; - const state = entry.state as InventoryState; - const instances = state?.instances ?? []; - const maxDur = props.breakable?.maxDurability ?? 100; - - output += `**${item.name}** (\`${item.key}\`)\n`; - - if (instances.length === 0) { - output += `⚠️ **CORRUPTO**: Quantity=${entry.quantity} pero sin instances\n`; - output += `• Usa \`!reset-inventory\` para reparar\n\n`; - continue; - } - - // Mostrar cada instancia con su durabilidad - instances.forEach((inst, idx) => { - const dur = inst.durability ?? 0; - const percentage = Math.round((dur / maxDur) * 100); - const bars = Math.floor(percentage / 10); - const barDisplay = "█".repeat(bars) + "░".repeat(10 - bars); - - output += ` [${ - idx + 1 - }] ${barDisplay} ${dur}/${maxDur} (${percentage}%)\n`; - }); - - output += `• Total: ${instances.length} unidad(es)\n\n`; - } - - // Dividir en chunks si es muy largo - const chunks = output.match(/[\s\S]{1,1900}/g) ?? [output]; - for (const chunk of chunks) { - await message.reply(chunk); - } - }, -}; diff --git a/src/.backup/game/effects.ts b/src/.backup/game/effects.ts deleted file mode 100644 index aca36b0..0000000 --- a/src/.backup/game/effects.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { - getActiveStatusEffects, - removeStatusEffect, - clearAllStatusEffects, -} from "../../../game/combat/statusEffectsService"; -import { consumeItemByKey } from "../../../game/economy/service"; - -// Item key que permite purgar efectos. Configurable más adelante. -const PURGE_ITEM_KEY = "potion.purga"; // placeholder - -export const command: CommandMessage = { - name: "efectos", - aliases: ["effects"], - type: "message", - cooldown: 5, - category: "Economía", - description: - "Lista tus efectos de estado activos y permite purgarlos con un ítem de purga.", - usage: "efectos [purgar|remover |todo]", - run: async (message, args, _client: Amayo) => { - const userId = message.author.id; - const guildId = message.guild!.id; - const sub = (args[0] || "").toLowerCase(); - - if ( - sub === "purgar" || - sub === "purga" || - sub === "remover" || - sub === "remove" || - sub === "todo" - ) { - // Requiere el item de purga - try { - const consume = await consumeItemByKey( - userId, - guildId, - PURGE_ITEM_KEY, - 1 - ); - if (!consume.consumed) { - await message.reply( - `Necesitas 1 **${PURGE_ITEM_KEY}** en tu inventario para purgar efectos.` - ); - return; - } - } catch { - await message.reply( - `No se pudo consumir el ítem de purga (${PURGE_ITEM_KEY}). Asegúrate de que existe.` - ); - return; - } - - // Modo remover tipo específico: efectos remover - if (sub === "remover" || sub === "remove") { - const typeArg = args[1]; - if (!typeArg) { - await message.reply("Debes indicar el tipo: efectos remover FATIGUE"); - return; - } - await removeStatusEffect(userId, guildId, typeArg.toUpperCase()); - await message.reply(`Efecto **${typeArg.toUpperCase()}** eliminado.`); - return; - } - - // Modo todo - if (sub === "todo" || sub === "purgar" || sub === "purga") { - await clearAllStatusEffects(userId, guildId); - await message.reply("Todos los efectos han sido purgados."); - return; - } - } - - // Listar efectos - const effects = await getActiveStatusEffects(userId, guildId); - if (!effects.length) { - await message.reply("No tienes efectos activos."); - return; - } - - const now = Date.now(); - const lines = effects.map((e) => { - let remain = "permanente"; - if (e.expiresAt) { - const ms = e.expiresAt.getTime() - now; - if (ms > 0) { - const m = Math.floor(ms / 60000); - const s = Math.floor((ms % 60000) / 1000); - remain = `${m}m ${s}s`; - } else remain = "exp"; - } - const pct = e.magnitude ? ` (${Math.round(e.magnitude * 100)}%)` : ""; - return `• ${e.type}${pct} - ${remain}`; - }); - - await message.reply( - `**Efectos Activos:**\n${lines.join( - "\n" - )}\n\nUsa: efectos purgar | efectos remover | efectos todo (requiere ${PURGE_ITEM_KEY}).` - ); - return; - }, -}; diff --git a/src/.backup/game/encantar.ts b/src/.backup/game/encantar.ts deleted file mode 100644 index 358bab3..0000000 --- a/src/.backup/game/encantar.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { applyMutationToInventory } from '../../../game/mutations/service'; -import { fetchItemBasics, formatItemLabel } from './_helpers'; - -export const command: CommandMessage = { - name: 'encantar', - type: 'message', - aliases: ['mutar','enchant'], - cooldown: 3, - description: 'Aplica una mutación/encantamiento a un ítem por su itemKey y mutationKey, respetando mutationPolicy.', - usage: 'encantar ', - run: async (message, args, _client: Amayo) => { - const itemKey = args[0]?.trim(); - const mutationKey = args[1]?.trim(); - if (!itemKey || !mutationKey) { await message.reply('Uso: `!encantar `'); return; } - const guildId = message.guild!.id; - const userId = message.author.id; - let itemInfo: { key: string; name: string | null; icon: string | null } = { key: itemKey, name: null, icon: null }; - try { - const basics = await fetchItemBasics(guildId, [itemKey]); - itemInfo = basics.get(itemKey) ?? itemInfo; - - await applyMutationToInventory(userId, guildId, itemKey, mutationKey); - const label = formatItemLabel(itemInfo, { bold: true }); - await message.reply(`✨ Aplicada mutación \`${mutationKey}\` a ${label}.`); - } catch (e: any) { - const label = formatItemLabel(itemInfo, { bold: true }); - await message.reply(`❌ No se pudo encantar ${label}: ${e?.message ?? e}`); - } - } -}; - diff --git a/src/.backup/game/equipar.ts b/src/.backup/game/equipar.ts deleted file mode 100644 index 2a1ef19..0000000 --- a/src/.backup/game/equipar.ts +++ /dev/null @@ -1,273 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { setEquipmentSlot } from "../../../game/combat/equipmentService"; -import { prisma } from "../../../core/database/prisma"; -import { formatItemLabel } from "./_helpers"; -import { ComponentType } from "discord-api-types/v10"; -import type { - MessageComponentInteraction, - StringSelectMenuInteraction, - TextBasedChannel, -} from "discord.js"; - -export const command: CommandMessage = { - name: "equipar", - type: "message", - aliases: ["equip"], - cooldown: 3, - description: - "Equipa un item en un slot (weapon|armor|cape) por su key, si lo tienes en inventario.", - usage: "equipar ", - run: async (message, args, _client: Amayo) => { - const guildId = message.guild!.id; - const userId = message.author.id; - - // Backwards compatible: if both args provided, keep old behavior - const maybeSlot = args[0]?.trim()?.toLowerCase() as - | "weapon" - | "armor" - | "cape" - | undefined; - const maybeItemKey = args[1]?.trim(); - if (maybeSlot && maybeItemKey) { - if (!["weapon", "armor", "cape"].includes(maybeSlot)) { - await message.reply("Uso: `!equipar `"); - return; - } - const item = await prisma.economyItem.findFirst({ - where: { key: maybeItemKey, OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); - if (!item) { - await message.reply("❌ Item no encontrado."); - return; - } - const inv = await prisma.inventoryEntry.findUnique({ - where: { userId_guildId_itemId: { userId, guildId, itemId: item.id } }, - }); - if (!inv || inv.quantity <= 0) { - await message.reply( - `❌ No tienes ${formatItemLabel(item, { - bold: true, - })} en tu inventario.` - ); - return; - } - await setEquipmentSlot(userId, guildId, maybeSlot, item.id); - await message.reply( - `🧰 Equipado en ${maybeSlot}: ${formatItemLabel(item, { bold: true })}` - ); - return; - } - - // Interactive flow: build select menus for slot and for items in user's inventory - const inventory = await prisma.inventoryEntry.findMany({ - where: { userId, guildId, quantity: { gt: 0 } }, - include: { item: true }, - }); - if (!inventory || inventory.length === 0) { - await message.reply("❌ No tienes items en el inventario para equipar."); - return; - } - - // Determine which slots the user actually has items for (based on item.tags) - const slotsSet = new Set(); - for (const inv of inventory) { - const tags = Array.isArray(inv.item.tags) ? inv.item.tags : []; - if (tags.includes("weapon")) slotsSet.add("weapon"); - if (tags.includes("armor")) slotsSet.add("armor"); - if (tags.includes("cape")) slotsSet.add("cape"); - } - const availableSlots = Array.from(slotsSet); - if (availableSlots.length === 0) { - await message.reply( - "❌ No tienes items equipables en el inventario (weapon/armor/cape)." - ); - return; - } - - const buildItemOptionsForSlot = (slot: string) => - inventory - .filter((inv) => { - const tags = Array.isArray(inv.item.tags) ? inv.item.tags : []; - return tags.includes(slot); - }) - .slice(0, 25) - .map((inv) => ({ - label: inv.item.name || inv.item.key, - value: inv.item.id, - description: inv.item.key, - })); - - const slotOptions = availableSlots.map((s) => { - if (s === "weapon") - return { - label: "Weapon (arma)", - value: "weapon", - description: "Equipar como arma", - }; - if (s === "armor") - return { - label: "Armor (armadura)", - value: "armor", - description: "Equipar como armadura", - }; - return { - label: "Cape (capa)", - value: "cape", - description: "Equipar como capa", - }; - }); - - const slotSelect = { - type: ComponentType.StringSelect, - custom_id: "equip_slot_select", - placeholder: "Selecciona el slot", - min_values: 1, - max_values: 1, - options: slotOptions, - } as any; - - // If only one slot available, preselect it and build item options for it. - let initialSelectedSlot: string | null = null; - if (availableSlots.length === 1) initialSelectedSlot = availableSlots[0]; - - const initialItemOptions = initialSelectedSlot - ? buildItemOptionsForSlot(initialSelectedSlot) - : // default to first slot's items so the select is populated - buildItemOptionsForSlot(availableSlots[0]); - - const itemSelect = { - type: ComponentType.StringSelect, - custom_id: "equip_item_select", - placeholder: initialSelectedSlot - ? "Selecciona el item a equipar" - : "Selecciona primero el slot (o usa el slot disponible)", - min_values: 1, - max_values: 1, - options: initialItemOptions, - } as any; - - const channel = message.channel as TextBasedChannel & { send: Function }; - const prompt = await (channel.send as any)({ - content: null, - flags: 32768, - reply: { messageReference: message.id }, - components: [ - { type: ComponentType.ActionRow, components: [slotSelect] }, - { type: ComponentType.ActionRow, components: [itemSelect] }, - ], - }); - - const collector = prompt.createMessageComponentCollector({ - time: 2 * 60_000, - filter: (i: MessageComponentInteraction) => - i.user.id === message.author.id, - }); - - let selectedSlot: string | null = initialSelectedSlot; - let selectedItemId: string | null = null; - - collector.on("collect", async (i: MessageComponentInteraction) => { - if (!i.isStringSelectMenu()) return; - await i.deferUpdate(); - if (i.customId === "equip_slot_select") { - selectedSlot = (i as StringSelectMenuInteraction).values[0]; - // rebuild item select options for the chosen slot - const newItemOptions = buildItemOptionsForSlot(selectedSlot); - const newSlotSelect = - prompt.components[0]?.components?.[0] ?? slotSelect; - const newItemSelect = { - type: ComponentType.StringSelect, - custom_id: "equip_item_select", - placeholder: "Selecciona el item a equipar", - min_values: 1, - max_values: 1, - options: newItemOptions, - } as any; - try { - await prompt.edit({ - content: `Slot seleccionado: **${selectedSlot}**`, - components: [ - { type: ComponentType.ActionRow, components: [newSlotSelect] }, - { type: ComponentType.ActionRow, components: [newItemSelect] }, - ], - }); - } catch {} - return; - } - if (i.customId === "equip_item_select") { - selectedItemId = (i as StringSelectMenuInteraction).values[0]; - if (!selectedSlot) { - // ask user to pick slot first - try { - await i.followUp({ - content: - "Por favor selecciona primero el slot donde quieres equipar.", - flags: 64, - }); - } catch {} - return; - } - - // validate that the selected item belongs to the chosen slot - const chosenItem = await prisma.economyItem.findUnique({ - where: { id: selectedItemId }, - }); - const chosenTags = Array.isArray(chosenItem?.tags) - ? chosenItem!.tags - : []; - if (!chosenTags.includes(selectedSlot)) { - try { - await prompt.edit({ - content: `❌ Ese ítem no puede equiparse en el slot **${selectedSlot}**.`, - components: [], - }); - } catch {} - collector.stop(); - return; - } - - // perform equip - try { - await setEquipmentSlot( - userId, - guildId, - selectedSlot as any, - selectedItemId - ); - // fetch item for label - const item = await prisma.economyItem.findUnique({ - where: { id: selectedItemId }, - }); - await prompt.edit({ - content: `🧰 Equipado en **${selectedSlot}**: ${ - item ? formatItemLabel(item, { bold: true }) : selectedItemId - }`, - components: [], - }); - } catch (e) { - console.error("Error equipping item:", e); - try { - await prompt.edit({ - content: `❌ Error al equipar: ${(e as any).message || e}`, - components: [], - }); - } catch {} - } - collector.stop(); - } - }); - - collector.on("end", async (_, reason) => { - if (reason === "time") { - try { - await prompt.edit({ - content: "Sesión de equipar expirada. Ejecuta `!equipar` de nuevo.", - components: [], - }); - } catch {} - } - }); - }, -}; diff --git a/src/.backup/game/fundir.ts b/src/.backup/game/fundir.ts deleted file mode 100644 index 499e92a..0000000 --- a/src/.backup/game/fundir.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { createSmeltJob } from '../../../game/smelting/service'; - -export const command: CommandMessage = { - name: 'fundir', - type: 'message', - aliases: ['smelt'], - cooldown: 5, - description: 'Crea un job de fundición: descuenta insumos y estará listo tras el tiempo indicado.', - usage: 'fundir : [inputKey2>: ...]' - + '\nEj: fundir ingot.iron 1 60 ore.iron:3', - run: async (message, args, _client: Amayo) => { - const [outputKey, qtyStr, secsStr, ...rest] = args; - if (!outputKey || !qtyStr || !secsStr || rest.length === 0) { - await message.reply('Uso: `!fundir : [...]`'); - return; - } - const outputQty = parseInt(qtyStr, 10); - const seconds = parseInt(secsStr, 10); - if (!Number.isFinite(outputQty) || outputQty <= 0 || !Number.isFinite(seconds) || seconds <= 0) { - await message.reply('❌ Cantidades/segundos inválidos.'); - return; - } - const inputs = rest.map((tok) => { - const [k, q] = tok.split(':'); - return { itemKey: (k || '').trim(), qty: Math.max(1, parseInt((q || '1'), 10) || 1) }; - }).filter(x => x.itemKey); - if (!inputs.length) { await message.reply('❌ Debes especificar al menos un insumo como key:qty'); return; } - - try { - const res = await createSmeltJob(message.author.id, message.guild!.id, inputs, outputKey, outputQty, seconds); - const when = new Date(res.readyAt).toLocaleTimeString('es-ES', { hour12: false }); - await message.reply(`🔥 Fundición creada (job: ${res.jobId}). Estará lista a las ${when}.`); - } catch (e: any) { - await message.reply(`❌ No se pudo crear la fundición: ${e?.message ?? e}`); - } - } -}; - diff --git a/src/.backup/game/fundirReclamar.ts b/src/.backup/game/fundirReclamar.ts deleted file mode 100644 index d331a41..0000000 --- a/src/.backup/game/fundirReclamar.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { claimSmeltJob, claimNextReadyJob } from '../../../game/smelting/service'; - -export const command: CommandMessage = { - name: 'fundir-reclamar', - type: 'message', - aliases: ['smelt-claim','reclamar-fundicion'], - cooldown: 3, - description: 'Reclama una fundición lista por jobId o la más antigua lista si no especificas id.', - usage: 'fundir-reclamar [jobId]' - + '\nSin argumentos intenta reclamar la más antigua lista.', - run: async (message, args, _client: Amayo) => { - const jobId = args[0]?.trim(); - try { - if (jobId) { - await claimSmeltJob(message.author.id, message.guild!.id, jobId); - await message.reply(`✅ Fundición reclamada (job ${jobId}).`); - } else { - const res = await claimNextReadyJob(message.author.id, message.guild!.id); - await message.reply(`✅ Fundición reclamada (job ${res.jobId}).`); - } - } catch (e: any) { - await message.reply(`❌ No se pudo reclamar: ${e?.message ?? e}`); - } - } -}; - diff --git a/src/.backup/game/inventario.ts b/src/.backup/game/inventario.ts deleted file mode 100644 index df88102..0000000 --- a/src/.backup/game/inventario.ts +++ /dev/null @@ -1,240 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { prisma } from "../../../core/database/prisma"; -import { getOrCreateWallet } from "../../../game/economy/service"; -import { - getEquipment, - getEffectiveStats, -} from "../../../game/combat/equipmentService"; -import type { ItemProps } from "../../../game/economy/types"; -import { - buildDisplay, - dividerBlock, - textBlock, -} from "../../../core/lib/componentsV2"; -import { sendDisplayReply, formatItemLabel } from "./_helpers"; - -const PAGE_SIZE = 15; - -import { parseItemProps } from "../../../game/core/utils"; - -function fmtTool(props: ItemProps) { - const t = props.tool; - if (!t) return ""; - const icon = - t.type === "pickaxe" - ? "⛏️" - : t.type === "rod" - ? "🎣" - : t.type === "sword" - ? "🗡️" - : t.type === "bow" - ? "🏹" - : t.type === "halberd" - ? "⚔️" - : t.type === "net" - ? "🕸️" - : "🔧"; - const tier = t.tier != null ? ` t${t.tier}` : ""; - return `${icon}${tier}`; -} - -function fmtStats(props: ItemProps) { - const parts: string[] = []; - if (typeof props.damage === "number" && props.damage > 0) - parts.push(`atk+${props.damage}`); - if (typeof props.defense === "number" && props.defense > 0) - parts.push(`def+${props.defense}`); - if (typeof props.maxHpBonus === "number" && props.maxHpBonus > 0) - parts.push(`hp+${props.maxHpBonus}`); - return parts.length ? ` (${parts.join(" ")})` : ""; -} - -const INVENTORY_ACCENT = 0xfee75c; - -export const command: CommandMessage = { - name: "inventario", - type: "message", - aliases: ["inv"], - cooldown: 3, - description: - 'Muestra tu inventario por servidor, con saldo y equipo. Usa "inv " o "inv ".', - usage: "inventario [página|filtro|itemKey]", - run: async (message, args, _client: Amayo) => { - const userId = message.author.id; - const guildId = message.guild!.id; - - const wallet = await getOrCreateWallet(userId, guildId); - const { weapon, armor, cape } = await getEquipment(userId, guildId); - const stats = await getEffectiveStats(userId, guildId); - - const arg = args[0]?.trim(); - const asPage = - arg && /^\d+$/.test(arg) ? Math.max(1, parseInt(arg, 10)) : 1; - const filter = arg && !/^\d+$/.test(arg) ? arg.toLowerCase() : ""; - - // detalle exacto si coincide completamente una key - let detailKey: string | null = null; - if (filter) detailKey = filter; // intentaremos exact match primero - - if (detailKey) { - const itemRow = await prisma.economyItem.findFirst({ - where: { key: detailKey, OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); - if (itemRow) { - const inv = await prisma.inventoryEntry.findUnique({ - where: { - userId_guildId_itemId: { userId, guildId, itemId: itemRow.id }, - }, - }); - const qty = inv?.quantity ?? 0; - const props = parseItemProps(itemRow.props); - const tool = fmtTool(props); - const st = fmtStats(props); - const tags = (itemRow.tags || []).join(", "); - const detailLines = [ - `**Cantidad:** x${qty}`, - `**Key:** \`${itemRow.key}\``, - itemRow.category ? `**Categoría:** ${itemRow.category}` : "", - tags ? `**Tags:** ${tags}` : "", - tool ? `**Herramienta:** ${tool}` : "", - st ? `**Bonos:** ${st}` : "", - props.craftingOnly ? "⚠️ Solo crafteo" : "", - ] - .filter(Boolean) - .join("\n"); - - const display = buildDisplay(INVENTORY_ACCENT, [ - textBlock(`# ${formatItemLabel(itemRow, { bold: true })}`), - dividerBlock(), - textBlock(detailLines || "*Sin información adicional.*"), - ]); - - await sendDisplayReply(message, display); - return; - } - } - - // listado paginado - const whereInv = { userId, guildId, quantity: { gt: 0 } } as const; - const all = await prisma.inventoryEntry.findMany({ - where: whereInv, - include: { item: true }, - }); - const filtered = filter - ? all.filter( - (e) => - e.item.key.toLowerCase().includes(filter) || - (e.item.name ?? "").toLowerCase().includes(filter) - ) - : all; - - const total = filtered.length; - const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); - const page = Math.min(asPage, totalPages); - const start = (page - 1) * PAGE_SIZE; - const pageItems = filtered - .sort( - (a, b) => - b.quantity - a.quantity || a.item.key.localeCompare(b.item.key) - ) - .slice(start, start + PAGE_SIZE); - - const gear: string[] = []; - if (weapon) - gear.push(`🗡️ ${formatItemLabel(weapon, { fallbackIcon: "" })}`); - if (armor) gear.push(`🛡️ ${formatItemLabel(armor, { fallbackIcon: "" })}`); - if (cape) gear.push(`🧥 ${formatItemLabel(cape, { fallbackIcon: "" })}`); - const headerLines = [ - `💰 Monedas: **${wallet.coins}**`, - gear.length ? `🧰 Equipo: ${gear.join(" · ")}` : "", - `❤️ HP: ${stats.hp}/${stats.maxHp} · ⚔️ ATK: ${stats.damage} · 🛡️ DEF: ${stats.defense}`, - filter ? `🔍 Filtro: ${filter}` : "", - ] - .filter(Boolean) - .join("\n"); - - const blocks = [ - textBlock("# 📦 Inventario"), - dividerBlock(), - textBlock(headerLines), - ]; - - if (!pageItems.length) { - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - blocks.push( - textBlock( - filter - ? `No hay ítems que coincidan con "${filter}".` - : "No tienes ítems en tu inventario." - ) - ); - const display = buildDisplay(INVENTORY_ACCENT, blocks); - await sendDisplayReply(message, display); - return; - } - - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - blocks.push( - textBlock( - `📦 Inventario (página ${page}/${totalPages}${ - filter ? `, filtro: ${filter}` : "" - })` - ) - ); - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - - pageItems.forEach((entry, index) => { - const props = parseItemProps(entry.item.props); - const tool = fmtTool(props); - const st = fmtStats(props); - const label = formatItemLabel(entry.item); - - // Mostrar durabilidad para items non-stackable con breakable - let qtyDisplay = `x${entry.quantity}`; - if ( - !entry.item.stackable && - props.breakable && - props.breakable.enabled !== false - ) { - const state = entry.state as any; - const instances = state?.instances ?? []; - if (instances.length > 0 && instances[0]?.durability != null) { - const firstDur = instances[0].durability; - const maxDur = props.breakable.maxDurability ?? 100; - qtyDisplay = `(${firstDur}/${maxDur})`; - if (instances.length > 1) { - qtyDisplay += ` x${instances.length}`; - } - } else if (instances.length === 0) { - qtyDisplay = `⚠️ CORRUPTO (x${entry.quantity})`; - } - } - - blocks.push( - textBlock(`• ${label} — ${qtyDisplay}${tool ? ` ${tool}` : ""}${st}`) - ); - if (index < pageItems.length - 1) { - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - } - }); - - if (totalPages > 1) { - const nextPage = Math.min(page + 1, totalPages); - const nextCommand = filter - ? `!inv ${nextPage} ${filter}` - : `!inv ${nextPage}`; - const backtick = "`"; - blocks.push(dividerBlock({ divider: false, spacing: 2 })); - blocks.push( - textBlock( - `💡 Usa ${backtick}${nextCommand}${backtick} para la siguiente página.` - ) - ); - } - - const display = buildDisplay(INVENTORY_ACCENT, blocks); - await sendDisplayReply(message, display); - }, -}; diff --git a/src/.backup/game/itemCreate.ts b/src/.backup/game/itemCreate.ts deleted file mode 100644 index 27acd21..0000000 --- a/src/.backup/game/itemCreate.ts +++ /dev/null @@ -1,791 +0,0 @@ -import { - Message, - MessageFlags, - MessageComponentInteraction, - ButtonInteraction, - TextBasedChannel, -} from "discord.js"; -import { - ComponentType, - TextInputStyle, - ButtonStyle, -} from "discord-api-types/v10"; -import type { CommandMessage } from "../../../core/types/commands"; -import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; -import logger from "../../../core/lib/logger"; -import type Amayo from "../../../core/client"; - -interface ItemEditorState { - key: string; - name?: string; - description?: string; - category?: string; - icon?: string; - stackable?: boolean; - maxPerInventory?: number | null; - tags: string[]; - props?: any; - // Nueva propiedad para receta de crafteo - recipe?: { - enabled: boolean; - ingredients: Array<{ itemKey: string; quantity: number }>; - productQuantity: number; - }; - // Derivado de props.global (solo owner puede establecerlo) - isGlobal?: boolean; -} - -export const command: CommandMessage = { - name: "item-crear", - type: "message", - aliases: ["crear-item", "itemcreate"], - cooldown: 10, - description: - "Crea un EconomyItem para este servidor con un pequeño editor interactivo.", - category: "Economía", - usage: "item-crear ", - run: async (message: Message, args: string[], client: Amayo) => { - const channel = message.channel as TextBasedChannel & { send: Function }; - const allowed = await hasManageGuildOrStaff( - message.member, - message.guild!.id, - client.prisma - ); - if (!allowed) { - await (channel.send as any)({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xff0000, - components: [ - { - type: 10, - content: - "❌ **Error de Permisos**\n└ No tienes permisos de ManageGuild ni rol de staff.", - }, - ], - }, - ], - reply: { messageReference: message.id }, - }); - return; - } - - const key = args[0]?.trim(); - if (!key) { - await (channel.send as any)({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xffa500, - components: [ - { - type: 10, - content: - "⚠️ **Uso Incorrecto**\n└ Uso: `!item-crear `", - }, - ], - }, - ], - reply: { messageReference: message.id }, - }); - return; - } - - const guildId = message.guild!.id; - - const exists = await client.prisma.economyItem.findFirst({ - where: { key, guildId }, - }); - if (exists) { - await (channel.send as any)({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xff0000, - components: [ - { - type: 10, - content: - "❌ **Item Ya Existe**\n└ Ya existe un item con esa key en este servidor.", - }, - ], - }, - ], - reply: { messageReference: message.id }, - }); - return; - } - - const state: ItemEditorState = { - key, - tags: [], - stackable: true, - maxPerInventory: null, - props: {}, - recipe: { - enabled: false, - ingredients: [], - productQuantity: 1, - }, - isGlobal: false, - }; - - const buildEditorDisplay = () => { - const baseInfo = [ - `**Nombre:** ${state.name || "*Sin definir*"}`, - `**Descripción:** ${state.description || "*Sin definir*"}`, - `**Categoría:** ${state.category || "*Sin definir*"}`, - `**Icon URL:** ${state.icon || "*Sin definir*"}`, - `**Stackable:** ${state.stackable ? "Sí" : "No"}`, - `**Máx. Inventario:** ${state.maxPerInventory ?? "Ilimitado"}`, - `**Global:** ${state.isGlobal ? "Sí" : "No"}`, - ].join("\n"); - - const tagsInfo = `**Tags:** ${ - state.tags.length > 0 ? state.tags.join(", ") : "*Ninguno*" - }`; - const propsJson = JSON.stringify(state.props ?? {}, null, 2); - const recipeInfo = state.recipe?.enabled - ? `**Receta:** Habilitada (${state.recipe.ingredients.length} ingredientes → ${state.recipe.productQuantity} unidades)` - : `**Receta:** Deshabilitada`; - - return { - type: 17, - accent_color: 0x00d9ff, - components: [ - { - type: 10, - content: `# 🛠️ Editor de Item: \`${key}\``, - }, - { type: 14, divider: true }, - { - type: 10, - content: baseInfo, - }, - { type: 14, divider: true }, - { - type: 10, - content: tagsInfo, - }, - { type: 14, divider: true }, - { - type: 10, - content: recipeInfo, - }, - { type: 14, divider: true }, - { - type: 10, - content: `**Props (JSON):**\n\`\`\`json\n${propsJson}\n\`\`\``, - }, - ], - }; - }; - - const buildEditorComponents = () => [ - buildEditorDisplay(), - { - type: 1, - components: [ - { - type: 2, - style: ButtonStyle.Primary, - label: "Base", - custom_id: "it_base", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Tags", - custom_id: "it_tags", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Receta", - custom_id: "it_recipe", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Props (JSON)", - custom_id: "it_props", - }, - ], - }, - { - type: 1, - components: [ - { - type: 2, - style: ButtonStyle.Success, - label: "Guardar", - custom_id: "it_save", - }, - { - type: 2, - style: ButtonStyle.Danger, - label: "Cancelar", - custom_id: "it_cancel", - }, - ], - }, - ]; - - const editorMsg = await (channel.send as any)({ - content: null, - flags: 32768, - components: buildEditorComponents(), - reply: { messageReference: message.id }, - }); - - const collector = editorMsg.createMessageComponentCollector({ - time: 30 * 60_000, - filter: (i) => i.user.id === message.author.id, - }); - - collector.on("collect", async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - if (i.customId === "it_cancel") { - await i.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xff0000, - components: [ - { - type: 10, - content: "**❌ Editor cancelado.**", - }, - ], - }, - ], - }); - collector.stop("cancel"); - return; - } - if (i.customId === "it_base") { - await showBaseModal( - i as ButtonInteraction, - state, - editorMsg, - buildEditorComponents - ); - return; - } - if (i.customId === "it_tags") { - await showTagsModal( - i as ButtonInteraction, - state, - editorMsg, - buildEditorComponents - ); - return; - } - if (i.customId === "it_recipe") { - await showRecipeModal( - i as ButtonInteraction, - state, - editorMsg, - buildEditorComponents, - client - ); - return; - } - if (i.customId === "it_props") { - await showPropsModal( - i as ButtonInteraction, - state, - editorMsg, - buildEditorComponents - ); - return; - } - if (i.customId === "it_save") { - // Validar - if (!state.name) { - await i.reply({ - content: "❌ Falta el nombre del item (configura en Base).", - flags: MessageFlags.Ephemeral, - }); - return; - } - // Revisar bandera global en props (puede haberse puesto manualmente en JSON) - state.isGlobal = !!state.props?.global; - const BOT_OWNER_ID = "327207082203938818"; - if (state.isGlobal && i.user.id !== BOT_OWNER_ID) { - await i.reply({ - content: - "❌ No puedes crear ítems globales. Solo el owner del bot.", - flags: MessageFlags.Ephemeral, - }); - return; - } - - // Si es global, usar guildId = null y verificar que no exista ya global con esa key - let targetGuildId: string | null = message.guild!.id; - if (state.isGlobal) { - const existsGlobal = await client.prisma.economyItem.findFirst({ - where: { key: state.key, guildId: null }, - }); - if (existsGlobal) { - await i.reply({ - content: "❌ Ya existe un ítem global con esa key.", - flags: MessageFlags.Ephemeral, - }); - return; - } - targetGuildId = null; - } - - // Guardar item - const createdItem = await client.prisma.economyItem.create({ - data: { - guildId: targetGuildId, - key: state.key, - name: state.name!, - description: state.description, - category: state.category, - icon: state.icon, - stackable: state.stackable ?? true, - maxPerInventory: state.maxPerInventory ?? undefined, - tags: state.tags, - props: state.props ?? {}, - }, - }); - - // Guardar receta si está habilitada - if (state.recipe?.enabled && state.recipe.ingredients.length > 0) { - try { - // Resolver itemIds de los ingredientes - const ingredientsData: Array<{ - itemId: string; - quantity: number; - }> = []; - for (const ing of state.recipe.ingredients) { - const item = await client.prisma.economyItem.findFirst({ - where: { - key: ing.itemKey, - OR: [{ guildId }, { guildId: null }], - }, - orderBy: [{ guildId: "desc" }], - }); - if (!item) { - throw new Error(`Ingrediente no encontrado: ${ing.itemKey}`); - } - ingredientsData.push({ - itemId: item.id, - quantity: ing.quantity, - }); - } - - // Crear la receta - await client.prisma.itemRecipe.create({ - data: { - productItemId: createdItem.id, - productQuantity: state.recipe.productQuantity, - ingredients: { - create: ingredientsData, - }, - }, - }); - } catch (err: any) { - logger.warn({ err }, "Error creando receta para item"); - await i.followUp({ - content: `⚠️ Item creado pero falló la receta: ${err.message}`, - flags: MessageFlags.Ephemeral, - }); - } - } - await i.reply({ - content: "✅ Item guardado!", - flags: MessageFlags.Ephemeral, - }); - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0x00ff00, - components: [ - { - type: 10, - content: `✅ **Item Creado**\n└ Item \`${ - state.key - }\` creado exitosamente.${ - state.isGlobal ? " (Global)" : "" - }`, - }, - ], - }, - ], - }); - collector.stop("saved"); - return; - } - } catch (err) { - logger.error({ err }, "item-crear interaction error"); - if (!i.deferred && !i.replied) - await i.reply({ - content: "❌ Error procesando la acción.", - flags: MessageFlags.Ephemeral, - }); - } - }); - - collector.on("end", async (_c, r) => { - if (r === "time") { - try { - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xffa500, - components: [ - { - type: 10, - content: "**⏰ Editor expirado.**", - }, - ], - }, - ], - }); - } catch {} - } - }); - }, -}; - -async function showBaseModal( - i: ButtonInteraction, - state: ItemEditorState, - editorMsg: any, - buildComponents: () => any[] -) { - const modal = { - title: "Configuración base del Item", - customId: "it_base_modal", - components: [ - { - type: ComponentType.Label, - label: "Nombre", - component: { - type: ComponentType.TextInput, - customId: "name", - style: TextInputStyle.Short, - required: true, - value: state.name ?? "", - }, - }, - { - type: ComponentType.Label, - label: "Descripción", - component: { - type: ComponentType.TextInput, - customId: "desc", - style: TextInputStyle.Paragraph, - required: false, - value: state.description ?? "", - }, - }, - { - type: ComponentType.Label, - label: "Categoría", - component: { - type: ComponentType.TextInput, - customId: "cat", - style: TextInputStyle.Short, - required: false, - value: state.category ?? "", - }, - }, - { - type: ComponentType.Label, - label: "Icon URL", - component: { - type: ComponentType.TextInput, - customId: "icon", - style: TextInputStyle.Short, - required: false, - value: state.icon ?? "", - }, - }, - { - type: ComponentType.Label, - label: "Stackable y Máx inventario", - component: { - type: ComponentType.TextInput, - customId: "stack_max", - style: TextInputStyle.Short, - required: false, - placeholder: "true,10", - value: - state.stackable !== undefined - ? `${state.stackable},${state.maxPerInventory ?? ""}` - : "", - }, - }, - ], - } as const; - - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - const name = sub.components.getTextInputValue("name").trim(); - const desc = sub.components.getTextInputValue("desc").trim(); - const cat = sub.components.getTextInputValue("cat").trim(); - const icon = sub.components.getTextInputValue("icon").trim(); - const stackMax = sub.components.getTextInputValue("stack_max").trim(); - - state.name = name; - state.description = desc || undefined; - state.category = cat || undefined; - state.icon = icon || undefined; - - if (stackMax) { - const [s, m] = stackMax.split(","); - state.stackable = String(s).toLowerCase() !== "false"; - const mv = m?.trim(); - state.maxPerInventory = mv ? Math.max(0, parseInt(mv, 10) || 0) : null; - } - - await sub.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents(), - }); - } catch {} -} - -async function showTagsModal( - i: ButtonInteraction, - state: ItemEditorState, - editorMsg: any, - buildComponents: () => any[] -) { - const modal = { - title: "Tags del Item (separados por coma)", - customId: "it_tags_modal", - components: [ - { - type: ComponentType.Label, - label: "Tags", - component: { - type: ComponentType.TextInput, - customId: "tags", - style: TextInputStyle.Paragraph, - required: false, - value: state.tags.join(", "), - }, - }, - ], - } as const; - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - const tags = sub.components.getTextInputValue("tags"); - state.tags = tags - ? tags - .split(",") - .map((t) => t.trim()) - .filter(Boolean) - : []; - await sub.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents(), - }); - } catch {} -} - -async function showPropsModal( - i: ButtonInteraction, - state: ItemEditorState, - editorMsg: any, - buildComponents: () => any[] -) { - const template = - state.props && Object.keys(state.props).length - ? JSON.stringify(state.props) - : JSON.stringify({ - tool: undefined, - breakable: undefined, - chest: undefined, - eventCurrency: undefined, - passiveEffects: [], - mutationPolicy: undefined, - craftingOnly: false, - food: undefined, - damage: undefined, - defense: undefined, - maxHpBonus: undefined, - }); - const modal = { - title: "Props (JSON) del Item", - customId: "it_props_modal", - components: [ - { - type: ComponentType.Label, - label: "JSON", - component: { - type: ComponentType.TextInput, - customId: "props", - style: TextInputStyle.Paragraph, - required: false, - value: template.slice(0, 4000), - }, - }, - ], - } as const; - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - const raw = sub.components.getTextInputValue("props"); - if (raw) { - try { - const parsed = JSON.parse(raw); - state.props = parsed; - await sub.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents(), - }); - } catch (e) { - await sub.reply({ - content: "❌ JSON inválido.", - flags: MessageFlags.Ephemeral, - }); - } - } else { - state.props = {}; - await sub.reply({ - content: "ℹ️ Props limpiados.", - flags: MessageFlags.Ephemeral, - }); - try { - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents(), - }); - } catch {} - } - } catch {} -} - -async function showRecipeModal( - i: ButtonInteraction, - state: ItemEditorState, - editorMsg: any, - buildComponents: () => any[], - client: Amayo -) { - const currentRecipe = state.recipe || { - enabled: false, - ingredients: [], - productQuantity: 1, - }; - const ingredientsStr = currentRecipe.ingredients - .map((ing) => `${ing.itemKey}:${ing.quantity}`) - .join(", "); - - const modal = { - title: "Receta de Crafteo", - customId: "it_recipe_modal", - components: [ - { - type: ComponentType.Label, - label: "Habilitar receta? (true/false)", - component: { - type: ComponentType.TextInput, - customId: "enabled", - style: TextInputStyle.Short, - required: false, - value: String(currentRecipe.enabled), - placeholder: "true o false", - }, - }, - { - type: ComponentType.Label, - label: "Cantidad que produce", - component: { - type: ComponentType.TextInput, - customId: "quantity", - style: TextInputStyle.Short, - required: false, - value: String(currentRecipe.productQuantity), - placeholder: "1", - }, - }, - { - type: ComponentType.Label, - label: "Ingredientes (itemKey:qty, ...)", - component: { - type: ComponentType.TextInput, - customId: "ingredients", - style: TextInputStyle.Paragraph, - required: false, - value: ingredientsStr, - placeholder: "iron_ingot:3, wood_plank:1", - }, - }, - ], - } as const; - - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - const enabledStr = sub.components - .getTextInputValue("enabled") - .trim() - .toLowerCase(); - const quantityStr = sub.components.getTextInputValue("quantity").trim(); - const ingredientsInput = sub.components - .getTextInputValue("ingredients") - .trim(); - - const enabled = enabledStr === "true"; - const productQuantity = parseInt(quantityStr, 10) || 1; - - // Parsear ingredientes - const ingredients: Array<{ itemKey: string; quantity: number }> = []; - if (ingredientsInput && enabled) { - const parts = ingredientsInput - .split(",") - .map((p) => p.trim()) - .filter(Boolean); - for (const part of parts) { - const [itemKey, qtyStr] = part.split(":").map((s) => s.trim()); - const qty = parseInt(qtyStr, 10); - if (itemKey && qty > 0) { - ingredients.push({ itemKey, quantity: qty }); - } - } - } - - state.recipe = { enabled, ingredients, productQuantity }; - - await sub.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents(), - }); - } catch {} -} diff --git a/src/.backup/game/itemEdit.ts b/src/.backup/game/itemEdit.ts deleted file mode 100644 index 84a9fdd..0000000 --- a/src/.backup/game/itemEdit.ts +++ /dev/null @@ -1,825 +0,0 @@ -import { - Message, - MessageFlags, - MessageComponentInteraction, - ButtonInteraction, - TextBasedChannel, -} from "discord.js"; -import { - ComponentType, - TextInputStyle, - ButtonStyle, -} from "discord-api-types/v10"; -import type { CommandMessage } from "../../../core/types/commands"; -import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; -import logger from "../../../core/lib/logger"; -import type Amayo from "../../../core/client"; -import { promptKeySelection, resolveItemIcon } from "./_helpers"; - -interface ItemEditorState { - key: string; - name?: string; - description?: string; - category?: string; - icon?: string; - stackable?: boolean; - maxPerInventory?: number | null; - tags: string[]; - props?: any; - // Nueva propiedad para receta de crafteo - recipe?: { - enabled: boolean; - ingredients: Array<{ itemKey: string; quantity: number }>; - productQuantity: number; - }; - isGlobal?: boolean; -} - -export const command: CommandMessage = { - name: "item-editar", - type: "message", - aliases: ["editar-item", "itemedit"], - cooldown: 10, - description: - "Edita un EconomyItem existente del servidor con un pequeño editor interactivo.", - category: "Economía", - usage: "item-editar", - run: async (message: Message, _args: string[], client: Amayo) => { - const channel = message.channel as TextBasedChannel & { send: Function }; - const allowed = await hasManageGuildOrStaff( - message.member, - message.guild!.id, - client.prisma - ); - if (!allowed) { - await (channel.send as any)({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xff0000, - components: [ - { - type: 10, - content: - "❌ **Error de Permisos**\n└ No tienes permisos de ManageGuild ni rol de staff.", - }, - ], - }, - ], - reply: { messageReference: message.id }, - }); - return; - } - - const guildId = message.guild!.id; - const items = await client.prisma.economyItem.findMany({ - where: { guildId }, - orderBy: [{ key: "asc" }], - }); - const selection = await promptKeySelection(message, { - entries: items, - customIdPrefix: "item_edit", - title: "Selecciona un ítem para editar", - emptyText: - "⚠️ **No hay ítems locales configurados.** Usa `!item-crear` primero.", - placeholder: "Elige un ítem…", - filterHint: "Filtra por nombre, key, categoría o tag.", - getOption: (item) => { - const icon = resolveItemIcon(item.icon); - const label = `${icon} ${item.name ?? item.key}`.trim(); - const tags = Array.isArray(item.tags) ? item.tags : []; - return { - value: item.id, - label: label.slice(0, 100), - description: item.key, - keywords: [item.key, item.name ?? "", item.category ?? "", ...tags], - }; - }, - }); - - if (!selection.entry || !selection.panelMessage) { - return; - } - - const existing = selection.entry; - - // Cargar receta si existe - let existingRecipe: { - ingredients: Array<{ item: { key: string }; quantity: number }>; - productQuantity: number; - } | null = null; - try { - existingRecipe = await client.prisma.itemRecipe.findUnique({ - where: { productItemId: existing.id }, - include: { ingredients: { include: { item: true } } }, - }); - } catch (e) { - logger.warn({ err: e }, "Error cargando receta existente"); - } - - const state: ItemEditorState = { - key: existing.key, - name: existing.name, - description: existing.description || undefined, - category: existing.category || undefined, - icon: existing.icon || undefined, - stackable: existing.stackable ?? true, - maxPerInventory: existing.maxPerInventory ?? null, - tags: Array.isArray(existing.tags) ? existing.tags : [], - props: existing.props || {}, - recipe: existingRecipe - ? { - enabled: true, - ingredients: existingRecipe.ingredients.map((ing) => ({ - itemKey: ing.item.key, - quantity: ing.quantity, - })), - productQuantity: existingRecipe.productQuantity, - } - : { - enabled: false, - ingredients: [], - productQuantity: 1, - }, - isGlobal: !!(existing.props as any)?.global || existing.guildId === null, - }; - - const buildEditorDisplay = () => { - const baseInfo = [ - `**Nombre:** ${state.name || "*Sin definir*"}`, - `**Descripción:** ${state.description || "*Sin definir*"}`, - `**Categoría:** ${state.category || "*Sin definir*"}`, - `**Icon URL:** ${state.icon || "*Sin definir*"}`, - `**Stackable:** ${state.stackable ? "Sí" : "No"}`, - `**Máx. Inventario:** ${state.maxPerInventory ?? "Ilimitado"}`, - `**Global:** ${state.isGlobal ? "Sí" : "No"}`, - ].join("\n"); - - const tagsInfo = `**Tags:** ${ - state.tags.length > 0 ? state.tags.join(", ") : "*Ninguno*" - }`; - const propsJson = JSON.stringify(state.props ?? {}, null, 2); - const recipeInfo = state.recipe?.enabled - ? `**Receta:** Habilitada (${state.recipe.ingredients.length} ingredientes → ${state.recipe.productQuantity} unidades)` - : `**Receta:** Deshabilitada`; - - return { - type: 17, - accent_color: 0x00d9ff, - components: [ - { - type: 10, - content: `# 🛠️ Editando Item: \`${state.key}\``, - }, - { type: 14, divider: true }, - { - type: 10, - content: baseInfo, - }, - { type: 14, divider: true }, - { - type: 10, - content: tagsInfo, - }, - { type: 14, divider: true }, - { - type: 10, - content: recipeInfo, - }, - { type: 14, divider: true }, - { - type: 10, - content: `**Props (JSON):**\n\`\`\`json\n${propsJson}\n\`\`\``, - }, - ], - }; - }; - - const buildEditorComponents = () => [ - buildEditorDisplay(), - { - type: 1, - components: [ - { - type: 2, - style: ButtonStyle.Primary, - label: "Base", - custom_id: "it_base", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Tags", - custom_id: "it_tags", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Receta", - custom_id: "it_recipe", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Props (JSON)", - custom_id: "it_props", - }, - ], - }, - { - type: 1, - components: [ - { - type: 2, - style: ButtonStyle.Success, - label: "Guardar", - custom_id: "it_save", - }, - { - type: 2, - style: ButtonStyle.Danger, - label: "Cancelar", - custom_id: "it_cancel", - }, - ], - }, - ]; - - const editorMsg = selection.panelMessage; - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildEditorComponents(), - }); - - const collector = editorMsg.createMessageComponentCollector({ - time: 30 * 60_000, - filter: (i) => i.user.id === message.author.id, - }); - - collector.on("collect", async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - if (i.customId === "it_cancel") { - await i.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xff0000, - components: [ - { - type: 10, - content: "**❌ Editor cancelado.**", - }, - ], - }, - ], - }); - collector.stop("cancel"); - return; - } - if (i.customId === "it_base") { - await showBaseModal( - i as ButtonInteraction, - state, - editorMsg, - buildEditorComponents - ); - return; - } - if (i.customId === "it_tags") { - await showTagsModal( - i as ButtonInteraction, - state, - editorMsg, - buildEditorComponents - ); - return; - } - if (i.customId === "it_recipe") { - await showRecipeModal( - i as ButtonInteraction, - state, - editorMsg, - buildEditorComponents, - client, - guildId, - existing.id - ); - return; - } - if (i.customId === "it_props") { - await showPropsModal( - i as ButtonInteraction, - state, - editorMsg, - buildEditorComponents - ); - return; - } - if (i.customId === "it_save") { - // Validar - if (!state.name) { - await i.reply({ - content: "❌ Falta el nombre del item (configura en Base).", - flags: MessageFlags.Ephemeral, - }); - return; - } - // Revalidar global flag (en caso de editar props JSON) - state.isGlobal = - !!(state.props as any)?.global || existing.guildId === null; - const BOT_OWNER_ID = "327207082203938818"; - if (state.isGlobal && i.user.id !== BOT_OWNER_ID) { - await i.reply({ - content: - "❌ No puedes editar un ítem global. Solo el owner del bot.", - flags: MessageFlags.Ephemeral, - }); - return; - } - // No permitir convertir un item local en global mediante edición si no es owner - if (!existing.guildId && !state.isGlobal) { - // Prevent accidental removal of global status - (state.props as any).global = true; - } - // Actualizar - await client.prisma.economyItem.update({ - where: { id: existing.id }, - data: { - name: state.name!, - description: state.description, - category: state.category, - icon: state.icon, - stackable: state.stackable ?? true, - maxPerInventory: state.maxPerInventory ?? undefined, - tags: state.tags, - props: state.props ?? {}, - }, - }); - - // Actualizar/crear/eliminar receta - try { - const existingRecipeCheck = - await client.prisma.itemRecipe.findUnique({ - where: { productItemId: existing.id }, - include: { ingredients: true }, - }); - - if (state.recipe?.enabled && state.recipe.ingredients.length > 0) { - // Resolver itemIds de los ingredientes - const ingredientsData: Array<{ - itemId: string; - quantity: number; - }> = []; - for (const ing of state.recipe.ingredients) { - const item = await client.prisma.economyItem.findFirst({ - where: { - key: ing.itemKey, - OR: [{ guildId }, { guildId: null }], - }, - orderBy: [{ guildId: "desc" }], - }); - if (!item) { - throw new Error(`Ingrediente no encontrado: ${ing.itemKey}`); - } - ingredientsData.push({ - itemId: item.id, - quantity: ing.quantity, - }); - } - - if (existingRecipeCheck) { - // Actualizar receta existente - // Primero eliminar ingredientes viejos - await client.prisma.recipeIngredient.deleteMany({ - where: { recipeId: existingRecipeCheck.id }, - }); - // Luego actualizar la receta con los nuevos ingredientes - await client.prisma.itemRecipe.update({ - where: { id: existingRecipeCheck.id }, - data: { - productQuantity: state.recipe.productQuantity, - ingredients: { - create: ingredientsData, - }, - }, - }); - } else { - // Crear nueva receta - await client.prisma.itemRecipe.create({ - data: { - productItemId: existing.id, - productQuantity: state.recipe.productQuantity, - ingredients: { - create: ingredientsData, - }, - }, - }); - } - } else if (existingRecipeCheck && !state.recipe?.enabled) { - // Eliminar receta si está deshabilitada - await client.prisma.recipeIngredient.deleteMany({ - where: { recipeId: existingRecipeCheck.id }, - }); - await client.prisma.itemRecipe.delete({ - where: { id: existingRecipeCheck.id }, - }); - } - } catch (err: any) { - logger.warn({ err }, "Error actualizando receta"); - await i.followUp({ - content: `⚠️ Item actualizado pero falló la receta: ${err.message}`, - flags: MessageFlags.Ephemeral, - }); - } - - await i.reply({ - content: "✅ Item actualizado!", - flags: MessageFlags.Ephemeral, - }); - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0x00ff00, - components: [ - { - type: 10, - content: `✅ **Item Actualizado**\n└ Item \`${ - state.key - }\` actualizado exitosamente.${ - state.isGlobal ? " (Global)" : "" - }`, - }, - ], - }, - ], - }); - collector.stop("saved"); - return; - } - } catch (err) { - logger.error({ err }, "item-editar interaction error"); - if (!i.deferred && !i.replied) - await i.reply({ - content: "❌ Error procesando la acción.", - flags: MessageFlags.Ephemeral, - }); - } - }); - - collector.on("end", async (_c, r) => { - if (r === "time") { - try { - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xffa500, - components: [ - { - type: 10, - content: "**⏰ Editor expirado.**", - }, - ], - }, - ], - }); - } catch {} - } - }); - }, -}; - -async function showBaseModal( - i: ButtonInteraction, - state: ItemEditorState, - editorMsg: any, - buildComponents: () => any[] -) { - const modal = { - title: "Configuración base del Item", - customId: "it_base_modal", - components: [ - { - type: ComponentType.Label, - label: "Nombre", - component: { - type: ComponentType.TextInput, - customId: "name", - style: TextInputStyle.Short, - required: true, - value: state.name ?? "", - }, - }, - { - type: ComponentType.Label, - label: "Descripción", - component: { - type: ComponentType.TextInput, - customId: "desc", - style: TextInputStyle.Paragraph, - required: false, - value: state.description ?? "", - }, - }, - { - type: ComponentType.Label, - label: "Categoría", - component: { - type: ComponentType.TextInput, - customId: "cat", - style: TextInputStyle.Short, - required: false, - value: state.category ?? "", - }, - }, - { - type: ComponentType.Label, - label: "Icon URL", - component: { - type: ComponentType.TextInput, - customId: "icon", - style: TextInputStyle.Short, - required: false, - value: state.icon ?? "", - }, - }, - { - type: ComponentType.Label, - label: "Stackable y Máx inventario", - component: { - type: ComponentType.TextInput, - customId: "stack_max", - style: TextInputStyle.Short, - required: false, - placeholder: "true,10", - value: - state.stackable !== undefined - ? `${state.stackable},${state.maxPerInventory ?? ""}` - : "", - }, - }, - ], - } as const; - - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - const name = sub.components.getTextInputValue("name").trim(); - const desc = sub.components.getTextInputValue("desc").trim(); - const cat = sub.components.getTextInputValue("cat").trim(); - const icon = sub.components.getTextInputValue("icon").trim(); - const stackMax = sub.components.getTextInputValue("stack_max").trim(); - - state.name = name; - state.description = desc || undefined; - state.category = cat || undefined; - state.icon = icon || undefined; - - if (stackMax) { - const [s, m] = stackMax.split(","); - state.stackable = String(s).toLowerCase() !== "false"; - const mv = m?.trim(); - state.maxPerInventory = mv ? Math.max(0, parseInt(mv, 10) || 0) : null; - } - - await sub.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents(), - }); - } catch {} -} - -async function showTagsModal( - i: ButtonInteraction, - state: ItemEditorState, - editorMsg: any, - buildComponents: () => any[] -) { - const modal = { - title: "Tags del Item (separados por coma)", - customId: "it_tags_modal", - components: [ - { - type: ComponentType.Label, - label: "Tags", - component: { - type: ComponentType.TextInput, - customId: "tags", - style: TextInputStyle.Paragraph, - required: false, - value: state.tags.join(", "), - }, - }, - ], - } as const; - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - const tags = sub.components.getTextInputValue("tags"); - state.tags = tags - ? tags - .split(",") - .map((t) => t.trim()) - .filter(Boolean) - : []; - await sub.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents(), - }); - } catch {} -} - -async function showPropsModal( - i: ButtonInteraction, - state: ItemEditorState, - editorMsg: any, - buildComponents: () => any[] -) { - const template = - state.props && Object.keys(state.props).length - ? JSON.stringify(state.props) - : JSON.stringify({ - tool: undefined, - breakable: undefined, - chest: undefined, - eventCurrency: undefined, - passiveEffects: [], - mutationPolicy: undefined, - craftingOnly: false, - food: undefined, - damage: undefined, - defense: undefined, - maxHpBonus: undefined, - }); - const modal = { - title: "Props (JSON) del Item", - customId: "it_props_modal", - components: [ - { - type: ComponentType.Label, - label: "JSON", - component: { - type: ComponentType.TextInput, - customId: "props", - style: TextInputStyle.Paragraph, - required: false, - value: template.slice(0, 4000), - }, - }, - ], - } as const; - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - const raw = sub.components.getTextInputValue("props"); - if (raw) { - try { - const parsed = JSON.parse(raw); - state.props = parsed; - await sub.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents(), - }); - } catch (e) { - await sub.reply({ - content: "❌ JSON inválido.", - flags: MessageFlags.Ephemeral, - }); - } - } else { - state.props = {}; - await sub.reply({ - content: "ℹ️ Props limpiados.", - flags: MessageFlags.Ephemeral, - }); - try { - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents(), - }); - } catch {} - } - } catch {} -} - -async function showRecipeModal( - i: ButtonInteraction, - state: ItemEditorState, - editorMsg: Message, - buildComponents: () => any[], - client: Amayo, - guildId: string, - itemId: string -) { - const currentRecipe = state.recipe || { - enabled: false, - ingredients: [], - productQuantity: 1, - }; - const ingredientsStr = currentRecipe.ingredients - .map((ing) => `${ing.itemKey}:${ing.quantity}`) - .join(", "); - - const modal = { - title: "Receta de Crafteo", - customId: "it_recipe_modal", - components: [ - { - type: ComponentType.Label, - label: "Habilitar receta? (true/false)", - component: { - type: ComponentType.TextInput, - customId: "enabled", - style: TextInputStyle.Short, - required: false, - value: String(currentRecipe.enabled), - placeholder: "true o false", - }, - }, - { - type: ComponentType.Label, - label: "Cantidad que produce", - component: { - type: ComponentType.TextInput, - customId: "quantity", - style: TextInputStyle.Short, - required: false, - value: String(currentRecipe.productQuantity), - placeholder: "1", - }, - }, - { - type: ComponentType.Label, - label: "Ingredientes (itemKey:qty, ...)", - component: { - type: ComponentType.TextInput, - customId: "ingredients", - style: TextInputStyle.Paragraph, - required: false, - value: ingredientsStr, - placeholder: "iron_ingot:3, wood_plank:1", - }, - }, - ], - } as const; - - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - const enabledStr = sub.components - .getTextInputValue("enabled") - .trim() - .toLowerCase(); - const quantityStr = sub.components.getTextInputValue("quantity").trim(); - const ingredientsInput = sub.components - .getTextInputValue("ingredients") - .trim(); - - const enabled = enabledStr === "true"; - const productQuantity = parseInt(quantityStr, 10) || 1; - - // Parsear ingredientes - const ingredients: Array<{ itemKey: string; quantity: number }> = []; - if (ingredientsInput && enabled) { - const parts = ingredientsInput - .split(",") - .map((p) => p.trim()) - .filter(Boolean); - for (const part of parts) { - const [itemKey, qtyStr] = part.split(":").map((s) => s.trim()); - const qty = parseInt(qtyStr, 10); - if (itemKey && qty > 0) { - ingredients.push({ itemKey, quantity: qty }); - } - } - } - - state.recipe = { enabled, ingredients, productQuantity }; - - await sub.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents(), - }); - } catch {} -} diff --git a/src/.backup/game/logros.ts b/src/.backup/game/logros.ts deleted file mode 100644 index 778f178..0000000 --- a/src/.backup/game/logros.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { getPlayerAchievements, getAchievementStats, createProgressBar } from '../../../game/achievements/service'; -import { EmbedBuilder } from 'discord.js'; - -export const command: CommandMessage = { - name: 'logros', - type: 'message', - aliases: ['achievements', 'logro', 'achievement'], - cooldown: 5, - description: 'Ver tus logros desbloqueados y progreso', - usage: 'logros [@usuario]', - run: async (message, args, client: Amayo) => { - try { - const guildId = message.guild!.id; - const targetUser = message.mentions.users.first() || message.author; - const userId = targetUser.id; - - // Obtener logros del jugador - const { unlocked, inProgress } = await getPlayerAchievements(userId, guildId); - const achievementStats = await getAchievementStats(userId, guildId); - - const embed = new EmbedBuilder() - .setColor(0xFFD700) - .setTitle(`🏆 Logros de ${targetUser.username}`) - .setThumbnail(targetUser.displayAvatarURL({ size: 128 })) - .setDescription( - `**${achievementStats.unlocked}/${achievementStats.total}** logros desbloqueados ` + - `(${achievementStats.percentage}%)\n` + - `⭐ **${achievementStats.points}** puntos totales` - ); - - // Logros desbloqueados recientes (últimos 5) - if (unlocked.length > 0) { - const recentUnlocked = unlocked.slice(0, 5); - let unlockedText = ''; - - for (const pa of recentUnlocked) { - const icon = pa.achievement.icon || '🏆'; - const points = pa.achievement.points || 10; - unlockedText += `${icon} **${pa.achievement.name}** (+${points} pts)\n`; - unlockedText += `└ ${pa.achievement.description}\n`; - } - - embed.addFields({ - name: `✅ Desbloqueados Recientes (${unlocked.length})`, - value: unlockedText || 'Ninguno aún', - inline: false - }); - } - - // Logros en progreso (top 5) - if (inProgress.length > 0) { - const topInProgress = inProgress.slice(0, 5); - let progressText = ''; - - for (const pa of topInProgress) { - const icon = pa.achievement.icon || '🔒'; - const req = pa.achievement.requirements as any; - const progress = pa.progress; - const required = req.value; - const bar = createProgressBar(progress, required, 8); - - progressText += `${icon} **${pa.achievement.name}**\n`; - progressText += `└ ${bar} (${progress}/${required})\n`; - } - - embed.addFields({ - name: `📈 En Progreso (${inProgress.length})`, - value: progressText, - inline: false - }); - } - - // Categorías - const categories = ['mining', 'fishing', 'combat', 'economy', 'exploration']; - const categoryEmojis: Record = { - mining: '⛏️', - fishing: '🎣', - combat: '⚔️', - economy: '💰', - exploration: '🗺️' - }; - - let categoryText = ''; - for (const cat of categories) { - const count = unlocked.filter(pa => pa.achievement.category === cat).length; - if (count > 0) { - categoryText += `${categoryEmojis[cat]} ${count} `; - } - } - - if (categoryText) { - embed.addFields({ - name: '📊 Por Categoría', - value: categoryText, - inline: false - }); - } - - if (unlocked.length === 0 && inProgress.length === 0) { - embed.setDescription( - 'Aún no has desbloqueado ningún logro.\n' + - '¡Empieza a jugar para obtener logros y puntos!' - ); - } - - embed.setFooter({ text: 'Los logros se desbloquean automáticamente al cumplir requisitos' }); - embed.setTimestamp(); - - await message.reply({ embeds: [embed] }); - } catch (error) { - console.error('Error en comando logros:', error); - await message.reply('❌ Error al obtener los logros.'); - } - } -}; diff --git a/src/.backup/game/mina.ts b/src/.backup/game/mina.ts deleted file mode 100644 index 1d75f06..0000000 --- a/src/.backup/game/mina.ts +++ /dev/null @@ -1,228 +0,0 @@ -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 { formatToolLabel, combatSummaryRPG } from "../../../game/lib/rpgFormat"; -import { buildAreaMetadataBlocks } from "./_helpers"; - -const MINING_ACCENT = 0xc27c0e; - -export const command: CommandMessage = { - 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)", - run: async (message, args, _client: Amayo) => { - const userId = message.author.id; - const guildId = message.guild!.id; - const { levelArg, providedTool, areaOverride } = parseGameArgs(args); - - const areaInfo = areaOverride - ? await resolveGuildAreaWithFallback(guildId, areaOverride) - : await resolveAreaByType(guildId, "MINE"); - - if (!areaInfo.area) { - if (areaOverride) { - 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:`." - ); - } - 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 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 rewardKeys = result.rewards - .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); - if (result.weaponTool?.key) rewardKeys.push(result.weaponTool.key); - const rewardItems = await fetchItemBasics(guildId, rewardKeys); - - // Actualizar stats - await updateStats(userId, guildId, { minesCompleted: 1 }); - - // Actualizar progreso de misiones - await updateQuestProgress(userId, guildId, "mine_count", 1); - - // Verificar logros - const newAchievements = await checkAchievements( - userId, - guildId, - "mine_count" - ); - - let 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") - : "• —"; - if (result.rewardModifiers?.baseCoinsAwarded != null) { - const { baseCoinsAwarded, coinsAfterPenalty, fatigueCoinMultiplier } = - result.rewardModifiers; - if ( - fatigueCoinMultiplier != null && - fatigueCoinMultiplier < 1 && - baseCoinsAwarded != null && - coinsAfterPenalty != null - ) { - const pct = Math.round((1 - fatigueCoinMultiplier) * 100); - rewardLines += `\n (⚠️ Fatiga: monedas base ${baseCoinsAwarded} → ${coinsAfterPenalty} (-${pct}%) )`; - } - } - const mobsLines = result.mobs.length - ? result.mobs.map((m) => `• ${m}`).join("\n") - : "• —"; - - const toolInfo = result.tool?.key - ? formatToolLabel({ - key: result.tool.key, - displayName: formatItemLabel( - rewardItems.get(result.tool.key) ?? { - key: result.tool.key, - name: null, - icon: null, - }, - { fallbackIcon: "🔧" } - ), - instancesRemaining: result.tool.instancesRemaining, - broken: result.tool.broken, - brokenInstance: result.tool.brokenInstance, - durabilityDelta: result.tool.durabilityDelta, - remaining: result.tool.remaining, - max: result.tool.max, - source: result.tool.toolSource, - }) - : "—"; - - const weaponInfo = result.weaponTool?.key - ? formatToolLabel({ - key: result.weaponTool.key, - displayName: formatItemLabel( - rewardItems.get(result.weaponTool.key) ?? { - key: result.weaponTool.key, - name: null, - icon: null, - }, - { fallbackIcon: "⚔️" } - ), - instancesRemaining: result.weaponTool.instancesRemaining, - broken: result.weaponTool.broken, - brokenInstance: result.weaponTool.brokenInstance, - durabilityDelta: result.weaponTool.durabilityDelta, - remaining: result.weaponTool.remaining, - max: result.weaponTool.max, - source: result.weaponTool.toolSource, - }) - : null; - - const combatSummary = result.combat - ? combatSummaryRPG({ - mobs: result.mobs.length, - mobsDefeated: result.combat.mobsDefeated, - totalDamageDealt: result.combat.totalDamageDealt, - totalDamageTaken: result.combat.totalDamageTaken, - playerStartHp: result.combat.playerStartHp, - playerEndHp: result.combat.playerEndHp, - outcome: result.combat.outcome, - }) - : null; - - const blocks = [textBlock("# ⛏️ Mina")]; - - if (globalNotice) { - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - blocks.push(textBlock(globalNotice)); - } - - blocks.push(dividerBlock()); - const areaScope = - source === "global" - ? "🌐 Configuración global" - : "📍 Configuración local"; - const toolsLine = weaponInfo - ? `**Pico:** ${toolInfo}\n**Arma (defensa):** ${weaponInfo}` - : `**Herramienta:** ${toolInfo}`; - blocks.push( - textBlock( - `**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n${toolsLine}` - ) - ); - 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}`)); - if (combatSummary) { - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - blocks.push(textBlock(combatSummary)); - } - - // Añadir metadata del área (imagen/descripcion) si existe - const metaBlocks = buildAreaMetadataBlocks(area); - if (metaBlocks.length) { - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - 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"); - blocks.push(textBlock(`🏆 ¡Logro desbloqueado!\n${achLines}`)); - } - - const display = buildDisplay(MINING_ACCENT, blocks); - await sendDisplayReply(message, display); - } catch (e: any) { - await message.reply(`❌ No se pudo minar: ${e?.message ?? e}`); - } - }, -}; diff --git a/src/.backup/game/misionReclamar.ts b/src/.backup/game/misionReclamar.ts deleted file mode 100644 index f39ff0d..0000000 --- a/src/.backup/game/misionReclamar.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { claimQuestReward, getPlayerQuests } from '../../../game/quests/service'; -import { EmbedBuilder } from 'discord.js'; -import { fetchItemBasics, formatItemLabel } from './_helpers'; - -export const command: CommandMessage = { - name: 'mision-reclamar', - type: 'message', - aliases: ['claim-quest', 'reclamar-mision'], - cooldown: 3, - description: 'Reclamar recompensa de misión completada', - usage: 'mision-reclamar ', - run: async (message, args, client: Amayo) => { - try { - const userId = message.author.id; - const guildId = message.guild!.id; - - if (!args[0]) { - await message.reply(`❌ Uso: \`!mision-reclamar \`\nEjemplo: \`!mision-reclamar 1\``); - return; - } - - // Obtener misiones completadas - const quests = await getPlayerQuests(userId, guildId); - const allQuests = [...quests.daily, ...quests.weekly, ...quests.permanent, ...quests.event]; - const claimable = allQuests.filter(q => q.canClaim); - - if (claimable.length === 0) { - await message.reply('❌ No tienes misiones listas para reclamar. Completa misiones primero usando los comandos del bot.'); - return; - } - - const index = parseInt(args[0]) - 1; - if (isNaN(index) || index < 0 || index >= claimable.length) { - await message.reply(`❌ Número de misión inválido. Elige un número entre 1 y ${claimable.length}.`); - return; - } - - const selected = claimable[index]; - - // Reclamar recompensa - const { quest, rewards } = await claimQuestReward(userId, guildId, selected.quest.id); - - const rewardData = (quest.rewards as any) ?? {}; - const formattedRewards: string[] = []; - if (rewardData.coins) formattedRewards.push(`💰 **${rewardData.coins.toLocaleString()}** monedas`); - if (rewardData.items && Array.isArray(rewardData.items) && rewardData.items.length) { - const basics = await fetchItemBasics(guildId, rewardData.items.map((item: any) => item.key)); - for (const item of rewardData.items) { - const info = basics.get(item.key) ?? { key: item.key, name: null, icon: null }; - const label = formatItemLabel(info, { bold: true }); - formattedRewards.push(`${label} ×${item.quantity}`); - } - } - if (rewardData.xp) formattedRewards.push(`⭐ **${rewardData.xp}** XP`); - if (rewardData.title) formattedRewards.push(`🏆 Título: **${rewardData.title}**`); - const rewardsDisplay = formattedRewards.length > 0 ? formattedRewards : rewards; - - const embed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('🎉 ¡Misión Completada!') - .setDescription(`Has reclamado las recompensas de **${quest.name}**`) - .setThumbnail(message.author.displayAvatarURL({ size: 128 })); - - // Mostrar recompensas - if (rewardsDisplay.length > 0) { - embed.addFields({ - name: '🎁 Recompensas Recibidas', - value: rewardsDisplay.join('\n'), - inline: false - }); - } - - // Info de la misión - embed.addFields( - { name: '📜 Misión', value: quest.description, inline: false } - ); - - embed.setFooter({ text: `Usa !misiones para ver más misiones` }); - embed.setTimestamp(); - - await message.reply({ embeds: [embed] }); - } catch (error: any) { - console.error('Error en comando mision-reclamar:', error); - await message.reply(`❌ ${error.message || 'Error al reclamar la misión.'}`); - } - } -}; diff --git a/src/.backup/game/misiones.ts b/src/.backup/game/misiones.ts deleted file mode 100644 index 41deb2a..0000000 --- a/src/.backup/game/misiones.ts +++ /dev/null @@ -1,157 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { getPlayerQuests, claimQuestReward } from '../../../game/quests/service'; -import { createProgressBar } from '../../../game/achievements/service'; -import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; - -export const command: CommandMessage = { - name: 'misiones', - type: 'message', - aliases: ['quests', 'mision', 'quest'], - cooldown: 5, - description: 'Ver misiones disponibles y tu progreso', - usage: 'misiones [categoria]', - run: async (message, args, client: Amayo) => { - try { - const userId = message.author.id; - const guildId = message.guild!.id; - - // Obtener misiones con progreso - const quests = await getPlayerQuests(userId, guildId); - - const embed = new EmbedBuilder() - .setColor(0x5865F2) - .setTitle('📜 Misiones Disponibles') - .setDescription(`${message.author.username}, aquí están tus misiones:`) - .setThumbnail(message.author.displayAvatarURL({ size: 128 })); - - // Emojis por categoría - const categoryEmojis: Record = { - mining: '⛏️', - fishing: '🎣', - combat: '⚔️', - economy: '💰', - exploration: '🗺️', - crafting: '🛠️' - }; - - // Función para formatear una lista de misiones - const formatQuests = (questList: any[], type: string) => { - if (questList.length === 0) return null; - - let text = ''; - for (const { quest, progress, canClaim, percentage } of questList) { - const icon = categoryEmojis[quest.category] || '📋'; - const req = quest.requirements as any; - const currentProgress = progress?.progress || 0; - const required = req.count; - - // Estado - let status = ''; - if (canClaim) { - status = '✅ ¡Listo para reclamar!'; - } else if (progress?.completed) { - status = '🎁 Completada'; - } else { - const bar = createProgressBar(currentProgress, required, 8); - status = `${bar}`; - } - - text += `${icon} **${quest.name}**\n`; - text += `└ ${quest.description}\n`; - text += `└ ${status}\n`; - - // Recompensas - const rewards = quest.rewards as any; - let rewardStr = ''; - if (rewards.coins) rewardStr += `💰 ${rewards.coins} `; - if (rewards.items && rewards.items.length > 0) { - rewardStr += `📦 ${rewards.items.length} items `; - } - if (rewardStr) { - text += `└ Recompensa: ${rewardStr}\n`; - } - - text += '\n'; - } - - return text; - }; - - // Misiones diarias - if (quests.daily.length > 0) { - const dailyText = formatQuests(quests.daily, 'daily'); - if (dailyText) { - embed.addFields({ - name: '📅 Misiones Diarias', - value: dailyText, - inline: false - }); - } - } - - // Misiones semanales - if (quests.weekly.length > 0) { - const weeklyText = formatQuests(quests.weekly, 'weekly'); - if (weeklyText) { - embed.addFields({ - name: '📆 Misiones Semanales', - value: weeklyText, - inline: false - }); - } - } - - // Misiones permanentes - if (quests.permanent.length > 0) { - const permanentText = formatQuests(quests.permanent.slice(0, 3), 'permanent'); - if (permanentText) { - embed.addFields({ - name: '♾️ Misiones Permanentes', - value: permanentText, - inline: false - }); - } - } - - // Misiones de evento - if (quests.event.length > 0) { - const eventText = formatQuests(quests.event, 'event'); - if (eventText) { - embed.addFields({ - name: '🎉 Misiones de Evento', - value: eventText, - inline: false - }); - } - } - - // Verificar si hay misiones para reclamar - const canClaim = [...quests.daily, ...quests.weekly, ...quests.permanent, ...quests.event] - .filter(q => q.canClaim); - - if (canClaim.length > 0) { - embed.addFields({ - name: '🎁 ¡Misiones Listas!', - value: `Tienes **${canClaim.length}** misiones listas para reclamar.\nUsa \`!mision-reclamar \` para reclamar recompensas.`, - inline: false - }); - } - - if (quests.daily.length === 0 && quests.weekly.length === 0 && quests.permanent.length === 0) { - embed.setDescription( - 'No hay misiones disponibles en este momento.\n' + - 'Las misiones diarias se generan automáticamente cada día.' - ); - } - - embed.setFooter({ text: 'Completa misiones para ganar recompensas' }); - embed.setTimestamp(); - - await message.reply({ embeds: [embed] }); - } catch (error) { - console.error('Error en comando misiones:', error); - await message.reply('❌ Error al obtener las misiones.'); - } - } -}; diff --git a/src/.backup/game/mobCreate.ts b/src/.backup/game/mobCreate.ts deleted file mode 100644 index 2f9b113..0000000 --- a/src/.backup/game/mobCreate.ts +++ /dev/null @@ -1,481 +0,0 @@ -import { - Message, - MessageFlags, - MessageComponentInteraction, - ButtonInteraction, - TextBasedChannel, -} from "discord.js"; -import { - ComponentType, - TextInputStyle, - ButtonStyle, -} from "discord-api-types/v10"; -import type { CommandMessage } from "../../../core/types/commands"; -import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; -import logger from "../../../core/lib/logger"; -import type Amayo from "../../../core/client"; - -interface MobEditorState { - key: string; - name?: string; - category?: string; - stats?: any; // JSON libre, ej: { attack, hp, defense } - drops?: any; // JSON libre, tabla de recompensas -} - -function createMobDisplay(state: MobEditorState, editing: boolean = false) { - const title = editing ? "Editando Mob" : "Creando Mob"; - const stats = state.stats || {}; - return { - type: 17, - accent_color: 0xff0000, - components: [ - { - type: 9, - components: [ - { - type: 10, - content: `👹 **${title}: \`${state.key}\`**`, - }, - ], - }, - { type: 14, divider: true }, - { - type: 9, - components: [ - { - type: 10, - content: - `**📋 Estado Actual:**\n` + - `**Nombre:** ${state.name || "❌ No configurado"}\n` + - `**Categoría:** ${state.category || "Sin categoría"}\n` + - `**Attack:** ${stats.attack || 0}\n` + - `**HP:** ${stats.hp || 0}\n` + - `**Defense:** ${stats.defense || 0}\n` + - `**Drops:** ${Object.keys(state.drops || {}).length} items`, - }, - ], - }, - { type: 14, divider: true }, - { - type: 9, - components: [ - { - type: 10, - content: - `**🎮 Instrucciones:**\n` + - `• **Base**: Nombre y categoría\n` + - `• **Stats (JSON)**: Estadísticas del mob\n` + - `• **Drops (JSON)**: Items que dropea\n` + - `• **Guardar**: Confirma los cambios\n` + - `• **Cancelar**: Descarta los cambios`, - }, - ], - }, - ], - }; -} - -export const command: CommandMessage = { - name: "mob-crear", - type: "message", - aliases: ["crear-mob", "mobcreate"], - cooldown: 10, - description: - "Crea un Mob (enemigo) para este servidor con editor interactivo.", - category: "Minijuegos", - usage: "mob-crear ", - run: async (message: Message, args: string[], client: Amayo) => { - const allowed = await hasManageGuildOrStaff( - message.member, - message.guild!.id, - client.prisma - ); - if (!allowed) { - await message.reply( - "❌ No tienes permisos de ManageGuild ni rol de staff." - ); - return; - } - const key = args[0]?.trim(); - if (!key) { - await message.reply("Uso: `!mob-crear `"); - return; - } - - const guildId = message.guild!.id; - const exists = await client.prisma.mob.findFirst({ - where: { key, guildId }, - }); - if (exists) { - await message.reply("❌ Ya existe un mob con esa key."); - return; - } - - const state: MobEditorState = { key, stats: { attack: 5 }, drops: {} }; - - const channel = message.channel as TextBasedChannel & { send: Function }; - const editorMsg = await channel.send({ - content: `👾 Editor de Mob: \`${key}\``, - components: [ - { - type: 1, - components: [ - { - type: 2, - style: ButtonStyle.Primary, - label: "Base", - custom_id: "mb_base", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Stats (JSON)", - custom_id: "mb_stats", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Drops (JSON)", - custom_id: "mb_drops", - }, - { - type: 2, - style: ButtonStyle.Success, - label: "Guardar", - custom_id: "mb_save", - }, - { - type: 2, - style: ButtonStyle.Danger, - label: "Cancelar", - custom_id: "mb_cancel", - }, - ], - }, - ], - }); - - const collector = editorMsg.createMessageComponentCollector({ - time: 30 * 60_000, - filter: (i) => i.user.id === message.author.id, - }); - collector.on("collect", async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - if (i.customId === "mb_cancel") { - await i.deferUpdate(); - await editorMsg.edit({ - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xff0000, - components: [ - { - type: 9, - components: [ - { - type: 10, - content: "**❌ Editor cancelado.**", - }, - ], - }, - ], - }, - ], - }); - collector.stop("cancel"); - return; - } - if (i.customId === "mb_base") { - await showBaseModal(i as ButtonInteraction, state, editorMsg, false); - return; - } - if (i.customId === "mb_stats") { - await showJsonModal( - i as ButtonInteraction, - state, - "stats", - "Stats del Mob (JSON)", - editorMsg, - false - ); - return; - } - if (i.customId === "mb_drops") { - await showJsonModal( - i as ButtonInteraction, - state, - "drops", - "Drops del Mob (JSON)", - editorMsg, - false - ); - return; - } - if (i.customId === "mb_save") { - if (!state.name) { - await i.reply({ - content: "❌ Falta el nombre del mob.", - flags: MessageFlags.Ephemeral, - }); - return; - } - // Use centralized admin createOrUpdate to persist mob (returns row when possible) - try { - const { createOrUpdateMob } = await import( - "../../../game/mobs/admin.js" - ); - await createOrUpdateMob({ ...(state as any), guildId }); - await i.reply({ - content: "✅ Mob guardado!", - flags: MessageFlags.Ephemeral, - }); - } catch (e) { - // fallback to direct Prisma if admin module not available - await client.prisma.mob.create({ - data: { - guildId, - key: state.key, - name: state.name!, - category: state.category ?? null, - stats: state.stats ?? {}, - drops: state.drops ?? {}, - }, - }); - await i.reply({ - content: "✅ Mob guardado (fallback)!", - flags: MessageFlags.Ephemeral, - }); - } - await editorMsg.edit({ - flags: 32768, - components: [ - { - type: 17, - accent_color: 0x00ff00, - components: [ - { - type: 9, - components: [ - { - type: 10, - content: `**✅ Mob \`${state.key}\` creado exitosamente.**`, - }, - ], - }, - ], - }, - ], - }); - collector.stop("saved"); - return; - } - } catch (err) { - logger.error({ err }, "mob-crear"); - if (!i.deferred && !i.replied) - await i.reply({ - content: "❌ Error procesando la acción.", - flags: MessageFlags.Ephemeral, - }); - } - }); - collector.on("end", async (_c, r) => { - if (r === "time") { - try { - await editorMsg.edit({ - content: "⏰ Editor expirado.", - components: [], - }); - } catch {} - } - }); - }, -}; - -async function showBaseModal( - i: ButtonInteraction, - state: MobEditorState, - editorMsg: Message, - editing: boolean -) { - const modal = { - title: "Base del Mob", - customId: "mb_base_modal", - components: [ - { - type: ComponentType.Label, - label: "Nombre", - component: { - type: ComponentType.TextInput, - customId: "name", - style: TextInputStyle.Short, - required: true, - value: state.name ?? "", - }, - }, - { - type: ComponentType.Label, - label: "Categoría (opcional)", - component: { - type: ComponentType.TextInput, - customId: "category", - style: TextInputStyle.Short, - required: false, - value: state.category ?? "", - }, - }, - ], - } as const; - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - state.name = sub.components.getTextInputValue("name").trim(); - const cat = sub.components.getTextInputValue("category")?.trim(); - state.category = cat || undefined; - await sub.reply({ - content: "✅ Base actualizada.", - flags: MessageFlags.Ephemeral, - }); - - // Refresh display - const newDisplay = createMobDisplay(state, editing); - await editorMsg.edit({ - flags: 32768, - components: [ - newDisplay, - { - type: 1, - components: [ - { - type: 2, - style: ButtonStyle.Primary, - label: "Base", - custom_id: "mb_base", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Stats (JSON)", - custom_id: "mb_stats", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Drops (JSON)", - custom_id: "mb_drops", - }, - { - type: 2, - style: ButtonStyle.Success, - label: "Guardar", - custom_id: "mb_save", - }, - { - type: 2, - style: ButtonStyle.Danger, - label: "Cancelar", - custom_id: "mb_cancel", - }, - ], - }, - ], - }); - } catch {} -} - -async function showJsonModal( - i: ButtonInteraction, - state: MobEditorState, - field: "stats" | "drops", - title: string, - editorMsg: Message, - editing: boolean -) { - const current = JSON.stringify(state[field] ?? {}); - const modal = { - title, - customId: `mb_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, - }); - return; - } - } else { - state[field] = {}; - await sub.reply({ content: "ℹ️ Limpio.", flags: MessageFlags.Ephemeral }); - } - - // Refresh display - const newDisplay = createMobDisplay(state, editing); - await editorMsg.edit({ - flags: 32768, - components: [ - newDisplay, - { - type: 1, - components: [ - { - type: 2, - style: ButtonStyle.Primary, - label: "Base", - custom_id: "mb_base", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Stats (JSON)", - custom_id: "mb_stats", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Drops (JSON)", - custom_id: "mb_drops", - }, - { - type: 2, - style: ButtonStyle.Success, - label: "Guardar", - custom_id: "mb_save", - }, - { - type: 2, - style: ButtonStyle.Danger, - label: "Cancelar", - custom_id: "mb_cancel", - }, - ], - }, - ], - }); - } catch {} -} diff --git a/src/.backup/game/mobDelete.ts b/src/.backup/game/mobDelete.ts deleted file mode 100644 index e92368e..0000000 --- a/src/.backup/game/mobDelete.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { - Message, - MessageFlags, - MessageComponentInteraction, - ButtonInteraction, - TextBasedChannel, -} from "discord.js"; -import { ButtonStyle, ComponentType } from "discord-api-types/v10"; -import type { CommandMessage } from "../../../core/types/commands"; -import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; -import logger from "../../../core/lib/logger"; -import type Amayo from "../../../core/client"; -import { promptKeySelection } from "./_helpers"; - -export const command: CommandMessage = { - name: "mob-eliminar", - type: "message", - aliases: ["eliminar-mob", "mobdelete"], - cooldown: 10, - description: "Elimina un mob del servidor (requiere permisos de staff)", - category: "Minijuegos", - usage: "mob-eliminar", - run: async (message: Message, _args: string[], client: Amayo) => { - const channel = message.channel as TextBasedChannel & { send: Function }; - const allowed = await hasManageGuildOrStaff( - message.member, - message.guild!.id, - client.prisma - ); - if (!allowed) { - await channel.send({ - content: undefined, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xff0000, - components: [ - { - type: 10, - content: - "❌ No tienes permisos de ManageGuild ni rol de staff.", - }, - ], - }, - ], - }); - return; - } - - const guildId = message.guild!.id; - try { - const { listMobsWithRows } = await import("../../../game/mobs/admin.js"); - const all = await listMobsWithRows(); - const localEntries = all.filter( - (e: any) => e.guildId === guildId && e.id - ); - const selection = await promptKeySelection(message, { - entries: localEntries, - customIdPrefix: "mob_delete", - title: "Selecciona un mob para eliminar", - emptyText: "⚠️ No hay mobs locales configurados.", - placeholder: "Elige un mob…", - filterHint: "Filtra por nombre, key o categoría.", - getOption: (entry: any) => ({ - value: entry.id ?? entry.def.key, - label: entry.def.name ?? entry.def.key, - description: [entry.def?.category ?? "Sin categoría", entry.def.key] - .filter(Boolean) - .join(" • "), - }), - }); - - if (!selection.entry) return; - const entry = selection.entry as any; - - // confirm - const confirmMsg = await channel.send({ - content: `¿Eliminar mob \`${ - entry.def.name || entry.def.key - }\`? Esta acción es irreversible.`, - components: [ - { - type: 1, - components: [ - { - type: 2, - style: ButtonStyle.Danger, - label: "Confirmar", - custom_id: "confirm_delete", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Cancelar", - custom_id: "cancel_delete", - }, - ], - }, - ], - }); - - const collector = confirmMsg.createMessageComponentCollector({ - time: 60_000, - filter: (i) => i.user.id === message.author.id, - }); - collector.on("collect", async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - if (i.customId === "cancel_delete") { - await i.update({ content: "❌ Cancelado.", components: [] }); - collector.stop("cancel"); - return; - } - if (i.customId === "confirm_delete") { - await i.deferUpdate(); - try { - const { deleteMob } = await import("../../../game/mobs/admin.js"); - const ok = await deleteMob(entry.def.key); - if (ok) { - await i.followUp({ - content: "✅ Mob eliminado.", - flags: MessageFlags.Ephemeral, - }); - try { - await confirmMsg.edit({ - content: "✅ Eliminado.", - components: [], - }); - } catch {} - } else { - // fallback to direct Prisma delete by id - await client.prisma.mob.delete({ where: { id: entry.id } }); - await i.followUp({ - content: "✅ Mob eliminado (fallback).", - flags: MessageFlags.Ephemeral, - }); - try { - await confirmMsg.edit({ - content: "✅ Eliminado (fallback).", - components: [], - }); - } catch {} - } - } catch (e: any) { - // If FK prevents deletion, inform user and suggest running cleanup script - const msg = (e && e.message) || String(e); - await i.followUp({ - content: `❌ No se pudo eliminar: ${msg}`, - flags: MessageFlags.Ephemeral, - }); - } - collector.stop("done"); - return; - } - } catch (err) { - logger.error({ err }, "mob-eliminar"); - } - }); - collector.on("end", async (_c, reason) => { - if (reason === "time") { - try { - await confirmMsg.edit({ - content: "⏰ Confirmación expirada.", - components: [], - }); - } catch {} - } - }); - } catch (e) { - logger.error({ e }, "mob-eliminar"); - await channel.send({ - content: "❌ Error al intentar eliminar mob.", - flags: 32768, - }); - } - }, -}; diff --git a/src/.backup/game/mobEdit.ts b/src/.backup/game/mobEdit.ts deleted file mode 100644 index d168f86..0000000 --- a/src/.backup/game/mobEdit.ts +++ /dev/null @@ -1,439 +0,0 @@ -import { - Message, - MessageFlags, - MessageComponentInteraction, - ButtonInteraction, - TextBasedChannel, -} from "discord.js"; -import { - ComponentType, - TextInputStyle, - ButtonStyle, -} from "discord-api-types/v10"; -import type { CommandMessage } from "../../../core/types/commands"; -import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; -import logger from "../../../core/lib/logger"; -import type Amayo from "../../../core/client"; -import { promptKeySelection } from "./_helpers"; - -interface MobEditorState { - key: string; - name?: string; - category?: string; - stats?: any; - drops?: any; -} -function createMobDisplay(state: MobEditorState, editing: boolean = false) { - const title = editing ? "Editando Mob" : "Creando Mob"; - const stats = state.stats || {}; - return { - type: 17, - accent_color: 0xff0000, - components: [ - { type: 10, content: `# 👹 ${title}: \`${state.key}\`` }, - { type: 14, divider: true }, - { - type: 10, - content: [ - "**📋 Estado Actual:**", - `**Nombre:** ${state.name || "❌ No configurado"}`, - `**Categoría:** ${state.category || "Sin categoría"}`, - `**Attack:** ${stats.attack || 0}`, - `**HP:** ${stats.hp || 0}`, - `**Defense:** ${stats.defense || 0}`, - `**Drops:** ${Object.keys(state.drops || {}).length} items`, - ].join("\n"), - }, - { type: 14, divider: true }, - { - type: 10, - content: [ - "**🎮 Instrucciones:**", - "• **Base**: Nombre y categoría", - "• **Stats (JSON)**: Estadísticas del mob", - "• **Drops (JSON)**: Items que dropea", - "• **Guardar**: Confirma los cambios", - "• **Cancelar**: Descarta los cambios", - ].join("\n"), - }, - ], - }; -} - -export const command: CommandMessage = { - name: "mob-editar", - type: "message", - aliases: ["editar-mob", "mobedit"], - cooldown: 10, - description: - "Edita un Mob (enemigo) de este servidor con editor interactivo.", - category: "Minijuegos", - usage: "mob-editar", - run: async (message: Message, _args: string[], client: Amayo) => { - const channel = message.channel as TextBasedChannel & { send: Function }; - const allowed = await hasManageGuildOrStaff( - message.member, - message.guild!.id, - client.prisma - ); - if (!allowed) { - await (channel.send as any)({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xff0000, - components: [ - { - type: 10, - content: - "❌ **Error de Permisos**\n└ No tienes permisos de ManageGuild ni rol de staff.", - }, - ], - }, - ], - reply: { messageReference: message.id }, - }); - return; - } - - const guildId = message.guild!.id; - const { listMobsWithRows } = await import("../../../game/mobs/admin.js"); - const all = await listMobsWithRows(); - // Keep behaviour: only guild-local mobs editable here - const localEntries = all.filter((e: any) => e.guildId === guildId); - const selection = await promptKeySelection(message, { - entries: localEntries, - customIdPrefix: "mob_edit", - title: "Selecciona un mob para editar", - emptyText: "⚠️ **No hay mobs configurados.** Usa `!mob-crear` primero.", - placeholder: "Elige un mob…", - filterHint: "Filtra por nombre, key o categoría.", - getOption: (entry: any) => ({ - value: entry.id ?? entry.def.key, - label: entry.def.name ?? entry.def.key, - description: [entry.def?.category ?? "Sin categoría", entry.def.key] - .filter(Boolean) - .join(" • "), - keywords: [ - entry.def.key, - entry.def.name ?? "", - entry.def?.category ?? "", - ], - }), - }); - - if (!selection.entry || !selection.panelMessage) { - return; - } - - const entry = selection.entry as any; - if (!entry) return; - - // If entry has an id (DB row), fetch the full row to get stats/drops stored in DB. - let dbRow: any = null; - if (entry.id) { - try { - dbRow = await client.prisma.mob.findUnique({ where: { id: entry.id } }); - } catch {} - } - - const state: MobEditorState = { - key: entry.def.key, - name: (dbRow && dbRow.name) ?? entry.def.name, - category: (dbRow && dbRow.category) ?? entry.def?.category ?? undefined, - stats: (dbRow && dbRow.stats) ?? entry.def?.base ?? {}, - drops: (dbRow && dbRow.drops) ?? entry.def?.drops ?? {}, - }; - - const buildEditorComponents = () => [ - createMobDisplay(state, true), - { - type: 1, - components: [ - { - type: 2, - style: ButtonStyle.Primary, - label: "Base", - custom_id: "mb_base", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Stats (JSON)", - custom_id: "mb_stats", - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "Drops (JSON)", - custom_id: "mb_drops", - }, - { - type: 2, - style: ButtonStyle.Success, - label: "Guardar", - custom_id: "mb_save", - }, - { - type: 2, - style: ButtonStyle.Danger, - label: "Cancelar", - custom_id: "mb_cancel", - }, - ], - }, - ]; - - const editorMsg = selection.panelMessage; - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildEditorComponents(), - }); - - const collector = editorMsg.createMessageComponentCollector({ - time: 30 * 60_000, - filter: (i) => i.user.id === message.author.id, - }); - collector.on("collect", async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - switch (i.customId) { - case "mb_cancel": - await i.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xff0000, - components: [ - { - type: 10, - content: "**❌ Editor cancelado.**", - }, - ], - }, - ], - }); - collector.stop("cancel"); - return; - case "mb_base": - await showBaseModal( - i as ButtonInteraction, - state, - editorMsg, - buildEditorComponents - ); - return; - case "mb_stats": - await showJsonModal( - i as ButtonInteraction, - state, - "stats", - "Stats del Mob (JSON)", - editorMsg, - buildEditorComponents - ); - return; - case "mb_drops": - await showJsonModal( - i as ButtonInteraction, - state, - "drops", - "Drops del Mob (JSON)", - editorMsg, - buildEditorComponents - ); - return; - case "mb_save": - if (!state.name) { - await i.reply({ - content: "❌ Falta el nombre del mob.", - flags: MessageFlags.Ephemeral, - }); - return; - } - try { - const { createOrUpdateMob } = await import( - "../../../game/mobs/admin.js" - ); - // Provide guildId so admin can scope or return db row - await createOrUpdateMob({ ...(state as any), guildId }); - await i.reply({ - content: "✅ Mob actualizado!", - flags: MessageFlags.Ephemeral, - }); - } catch (e) { - // fallback to direct update - await client.prisma.mob.update({ - where: { id: entry.id }, - data: { - name: state.name!, - category: state.category ?? null, - stats: state.stats ?? {}, - drops: state.drops ?? {}, - }, - }); - await i.reply({ - content: "✅ Mob actualizado (fallback)!", - flags: MessageFlags.Ephemeral, - }); - } - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0x00ff00, - components: [ - { - type: 10, - content: `**✅ Mob \`${state.key}\` actualizado exitosamente.**`, - }, - ], - }, - ], - }); - collector.stop("saved"); - return; - } - } catch (err) { - logger.error({ err }, "mob-editar"); - if (!i.deferred && !i.replied) - await i.reply({ - content: "❌ Error procesando la acción.", - flags: MessageFlags.Ephemeral, - }); - } - }); - collector.on("end", async (_c, reason) => { - if (reason === "time") { - try { - await editorMsg.edit({ - content: null, - flags: 32768, - components: [ - { - type: 17, - accent_color: 0xffa500, - components: [ - { - type: 10, - content: "**⏰ Editor expirado.**", - }, - ], - }, - ], - }); - } catch {} - } - }); - }, -}; - -async function showBaseModal( - i: ButtonInteraction, - state: MobEditorState, - editorMsg: Message, - buildComponents: () => any[] -) { - const modal = { - title: "Base del Mob", - customId: "mb_base_modal", - components: [ - { - type: ComponentType.Label, - label: "Nombre", - component: { - type: ComponentType.TextInput, - customId: "name", - style: TextInputStyle.Short, - required: true, - value: state.name ?? "", - }, - }, - { - type: ComponentType.Label, - label: "Categoría (opcional)", - component: { - type: ComponentType.TextInput, - customId: "category", - style: TextInputStyle.Short, - required: false, - value: state.category ?? "", - }, - }, - ], - } as const; - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - state.name = sub.components.getTextInputValue("name").trim(); - const cat = sub.components.getTextInputValue("category")?.trim(); - state.category = cat || undefined; - await sub.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents(), - }); - } catch {} -} - -async function showJsonModal( - i: ButtonInteraction, - state: MobEditorState, - field: "stats" | "drops", - title: string, - editorMsg: Message, - buildComponents: () => any[] -) { - const current = JSON.stringify(state[field] ?? {}); - const modal = { - title, - customId: `mb_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.deferUpdate(); - } catch { - await sub.reply({ - content: "❌ JSON inválido.", - flags: MessageFlags.Ephemeral, - }); - return; - } - } else { - state[field] = {}; - await sub.deferUpdate(); - } - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents(), - }); - } catch {} -} diff --git a/src/.backup/game/monedas.ts b/src/.backup/game/monedas.ts deleted file mode 100644 index eeff1bf..0000000 --- a/src/.backup/game/monedas.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { getOrCreateWallet } from "../../../game/economy/service"; -import type { TextBasedChannel } from "discord.js"; - -export const command: CommandMessage = { - name: "monedas", - type: "message", - aliases: ["coins", "saldo"], - cooldown: 2, - description: "Muestra tu saldo de monedas en este servidor.", - category: "Economía", - usage: "monedas", - run: async (message, _args, _client: Amayo) => { - const wallet = await getOrCreateWallet( - message.author.id, - message.guild!.id - ); - - const display = { - type: 17, - accent_color: 0xffd700, - components: [ - { - type: 9, - components: [ - { - type: 10, - content: `**<:coin:1425667511013081169> Monedas de ${ - message.author.username - }**\n\nSaldo: **${wallet.coins.toLocaleString()}** monedas`, - }, - ], - }, - ], - }; - - const channel = message.channel as TextBasedChannel & { send: Function }; - await (channel.send as any)({ - display, - flags: 32768, - reply: { messageReference: message.id }, - }); - }, -}; diff --git a/src/.backup/game/offerCreate.ts b/src/.backup/game/offerCreate.ts deleted file mode 100644 index 83b60a7..0000000 --- a/src/.backup/game/offerCreate.ts +++ /dev/null @@ -1,129 +0,0 @@ -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 OfferState { - itemKey?: string; - enabled?: boolean; - price?: any; - startAt?: string; - endAt?: string; - perUserLimit?: number | null; - stock?: number | null; - metadata?: any; -} - -export const command: CommandMessage = { - name: 'offer-crear', - type: 'message', - aliases: ['crear-oferta','ofertacreate'], - cooldown: 10, - description: 'Crea una ShopOffer para este servidor con editor interactivo (price/ventanas/stock/limit).', - usage: 'offer-crear', - 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 guildId = message.guild!.id; - const state: OfferState = { enabled: true, price: {}, perUserLimit: null, stock: null, metadata: {} }; - - const editorMsg = await (message.channel as any).send({ - content: `🛒 Editor de Oferta (crear)`, - components: [ - { type: 1, components: [ - { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'of_base' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Precio (JSON)', custom_id: 'of_price' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'of_window' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Límites', custom_id: 'of_limits' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'of_meta' }, - ]}, - { type: 1, components: [ - { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'of_save' }, - { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'of_cancel' }, - ]}, - ], - }); - - const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i)=> i.user.id === message.author.id }); - collector.on('collect', async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - switch (i.customId) { - case 'of_cancel': await i.deferUpdate(); await editorMsg.edit({ content: '❌ Editor de Oferta cancelado.', components: [] }); collector.stop('cancel'); return; - case 'of_base': await showBaseModal(i as ButtonInteraction, state); return; - case 'of_price': await showJsonModal(i as ButtonInteraction, state, 'price', 'Precio'); return; - case 'of_window': await showWindowModal(i as ButtonInteraction, state); return; - case 'of_limits': await showLimitsModal(i as ButtonInteraction, state); return; - case 'of_meta': await showJsonModal(i as ButtonInteraction, state, 'metadata', 'Meta'); return; - case 'of_save': - if (!state.itemKey) { await i.reply({ content: '❌ Falta itemKey en Base.', flags: MessageFlags.Ephemeral }); return; } - const item = await prisma.economyItem.findFirst({ where: { key: state.itemKey, OR: [{ guildId }, { guildId: null }] }, orderBy: [{ guildId: 'desc' }] }); - if (!item) { await i.reply({ content: '❌ Item no encontrado por key.', flags: MessageFlags.Ephemeral }); return; } - try { - await prisma.shopOffer.create({ - data: { - guildId, - itemId: item.id, - enabled: state.enabled ?? true, - price: state.price ?? {}, - startAt: state.startAt ? new Date(state.startAt) : null, - endAt: state.endAt ? new Date(state.endAt) : null, - perUserLimit: state.perUserLimit ?? null, - stock: state.stock ?? null, - metadata: state.metadata ?? {}, - } - }); - await i.reply({ content: '✅ Oferta guardada.', flags: MessageFlags.Ephemeral }); - await editorMsg.edit({ content: `✅ Oferta creada para ${state.itemKey}.`, components: [] }); - collector.stop('saved'); - } catch (err: any) { - await i.reply({ content: `❌ Error al guardar: ${err?.message ?? err}`, flags: MessageFlags.Ephemeral }); - } - return; - } - } catch (e) { - if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral }); - } - }); - collector.on('end', async (_c,r)=> { if (r==='time') { try { await editorMsg.edit({ content:'⏰ Editor expirado.', components: [] }); } catch {} } }); - } -}; - -async function showBaseModal(i: ButtonInteraction, state: OfferState) { - const modal = { title: 'Base de Oferta', customId: 'of_base_modal', components: [ - { type: ComponentType.Label, label: 'Item Key', component: { type: ComponentType.TextInput, customId: 'itemKey', style: TextInputStyle.Short, required: true, value: state.itemKey ?? '' } }, - { type: ComponentType.Label, label: 'Habilitada? (true/false)', component: { type: ComponentType.TextInput, customId: 'enabled', style: TextInputStyle.Short, required: false, value: String(state.enabled ?? true) } }, - ] } as const; - await i.showModal(modal); - try { const sub = await i.awaitModalSubmit({ time: 300_000 }); state.itemKey = sub.components.getTextInputValue('itemKey').trim(); const en = sub.components.getTextInputValue('enabled').trim(); state.enabled = en ? (en.toLowerCase() !== 'false') : (state.enabled ?? true); await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral }); } catch {} -} - -async function showJsonModal(i: ButtonInteraction, state: OfferState, field: 'price'|'metadata', title: string) { - const current = JSON.stringify(state[field] ?? {}); - const modal = { title, customId: `of_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 }); } } catch {} -} - -async function showWindowModal(i: ButtonInteraction, state: OfferState) { - const modal = { title: 'Ventana', customId: 'of_window_modal', components: [ - { type: ComponentType.Label, label: 'Inicio (ISO opcional)', component: { type: ComponentType.TextInput, customId: 'start', style: TextInputStyle.Short, required: false, value: state.startAt ?? '' } }, - { type: ComponentType.Label, label: 'Fin (ISO opcional)', component: { type: ComponentType.TextInput, customId: 'end', style: TextInputStyle.Short, required: false, value: state.endAt ?? '' } }, - ] } as const; - await i.showModal(modal); - try { const sub = await i.awaitModalSubmit({ time: 300_000 }); const s = sub.components.getTextInputValue('start').trim(); const e = sub.components.getTextInputValue('end').trim(); state.startAt = s || ''; state.endAt = e || ''; await sub.reply({ content: '✅ Ventana actualizada.', flags: MessageFlags.Ephemeral }); } catch {} -} - -async function showLimitsModal(i: ButtonInteraction, state: OfferState) { - const modal = { title: 'Límites', customId: 'of_limits_modal', components: [ - { type: ComponentType.Label, label: 'Límite por usuario (vacío = sin límite)', component: { type: ComponentType.TextInput, customId: 'limit', style: TextInputStyle.Short, required: false, value: state.perUserLimit != null ? String(state.perUserLimit) : '' } }, - { type: ComponentType.Label, label: 'Stock global (vacío = ilimitado)', component: { type: ComponentType.TextInput, customId: 'stock', style: TextInputStyle.Short, required: false, value: state.stock != null ? String(state.stock) : '' } }, - ] } as const; - await i.showModal(modal); - try { const sub = await i.awaitModalSubmit({ time: 300_000 }); const lim = sub.components.getTextInputValue('limit').trim(); const st = sub.components.getTextInputValue('stock').trim(); state.perUserLimit = lim ? Math.max(0, parseInt(lim,10)||0) : null; state.stock = st ? Math.max(0, parseInt(st,10)||0) : null; await sub.reply({ content: '✅ Límites actualizados.', flags: MessageFlags.Ephemeral }); } catch {} -} diff --git a/src/.backup/game/offerEdit.ts b/src/.backup/game/offerEdit.ts deleted file mode 100644 index ea70ef4..0000000 --- a/src/.backup/game/offerEdit.ts +++ /dev/null @@ -1,339 +0,0 @@ -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, TextBasedChannel } from 'discord.js'; -import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10'; -import { promptKeySelection, resolveItemIcon } from './_helpers'; - -interface OfferState { - offerId: string; - itemKey?: string; - enabled?: boolean; - price?: any; - startAt?: string; - endAt?: string; - perUserLimit?: number | null; - stock?: number | null; - metadata?: any; -} - -export const command: CommandMessage = { - name: 'offer-editar', - type: 'message', - aliases: ['editar-oferta','offeredit'], - cooldown: 10, - description: 'Edita una ShopOffer por ID con editor interactivo (price/ventanas/stock/limit).', - usage: 'offer-editar', - run: async (message, _args, _client: Amayo) => { - const channel = message.channel as TextBasedChannel & { send: Function }; - const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, prisma); - if (!allowed) { - await (channel.send as any)({ - content: null, - flags: 32768, - components: [{ - type: 17, - accent_color: 0xFF0000, - components: [{ - type: 10, - content: '❌ **Error de Permisos**\n└ No tienes permisos de ManageGuild ni rol de staff.' - }] - }], - reply: { messageReference: message.id } - }); - return; - } - - const guildId = message.guild!.id; - const offers = await prisma.shopOffer.findMany({ - where: { guildId }, - orderBy: [{ updatedAt: 'desc' }], - include: { item: true }, - }); - - const selection = await promptKeySelection(message, { - entries: offers, - customIdPrefix: 'offer_edit', - title: 'Selecciona una oferta para editar', - emptyText: '⚠️ **No hay ofertas configuradas.** Usa `!offer-crear` primero.', - placeholder: 'Elige una oferta…', - filterHint: 'Filtra por item, key o estado.', - getOption: (offer) => { - const icon = resolveItemIcon(offer.item?.icon); - const itemName = offer.item?.name ?? offer.item?.key ?? 'Item sin nombre'; - const status = offer.enabled ? 'Activa' : 'Inactiva'; - const label = `${icon} ${itemName}`.trim(); - return { - value: offer.id, - label: label.slice(0, 100), - description: `${status} • ${offer.id.slice(0, 14)}…`, - keywords: [offer.id, itemName, offer.item?.key ?? '', status], - }; - }, - }); - - if (!selection.entry || !selection.panelMessage) { - return; - } - - const offer = selection.entry; - - const state: OfferState = { - offerId: offer.id, - itemKey: offer.item?.key, - enabled: offer.enabled, - price: offer.price ?? {}, - startAt: offer.startAt ? new Date(offer.startAt).toISOString() : '', - endAt: offer.endAt ? new Date(offer.endAt).toISOString() : '', - perUserLimit: offer.perUserLimit ?? null, - stock: offer.stock ?? null, - metadata: offer.metadata ?? {}, - }; - - const buildEditorDisplay = () => { - const status = state.enabled ? '✅ Activa' : '⛔ Inactiva'; - const priceInfo = state.price && Object.keys(state.price ?? {}).length ? 'Configurado' : 'Sin configurar'; - const windowInfo = state.startAt || state.endAt ? `${state.startAt || '—'} → ${state.endAt || '—'}` : 'Sin ventana'; - const limitsInfo = `Usuario: ${state.perUserLimit ?? '∞'} • Stock: ${state.stock ?? '∞'}`; - const metaInfo = state.metadata && Object.keys(state.metadata ?? {}).length ? `${Object.keys(state.metadata!).length} campos` : 'Vacío'; - return { - type: 17, - accent_color: state.enabled ? 0x00D9FF : 0x666666, - components: [ - { - type: 10, - content: `# 🛒 Editando Oferta: \`${state.offerId}\`\n**Item:** ${state.itemKey ?? '*Sin asignar*'}\n**Estado:** ${status}\n**Precio:** ${priceInfo}\n**Ventana:** ${windowInfo}\n**Límites:** ${limitsInfo}\n**Meta:** ${metaInfo}` - }, - { type: 14, divider: true }, - { - type: 10, - content: '**📋 Pasos rápidos:**\n• Base: item y estado\n• Precio: JSON de coste\n• Ventana: fechas inicio/fin\n• Límites: stock y por usuario\n• Meta: datos adicionales' - } - ] - }; - }; - - const buildEditorComponents = () => [ - buildEditorDisplay(), - { - type: 1, - components: [ - { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'of_base' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Precio (JSON)', custom_id: 'of_price' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'of_window' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Límites', custom_id: 'of_limits' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'of_meta' }, - ] - }, - { - type: 1, - components: [ - { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'of_save' }, - { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'of_cancel' }, - ] - } - ]; - - const editorMsg = selection.panelMessage; - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildEditorComponents(), - }); - - 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 'of_cancel': - await i.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: [{ - type: 17, - accent_color: 0xFF0000, - components: [{ - type: 10, - content: '**❌ Editor de Oferta cancelado.**' - }] - }] - }); - collector.stop('cancel'); - return; - case 'of_base': - await showBaseModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents); - return; - case 'of_price': - await showJsonModal(i as ButtonInteraction, state, 'price', 'Precio', editorMsg, buildEditorComponents); - return; - case 'of_window': - await showWindowModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents); - return; - case 'of_limits': - await showLimitsModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents); - return; - case 'of_meta': - await showJsonModal(i as ButtonInteraction, state, 'metadata', 'Meta', editorMsg, buildEditorComponents); - return; - case 'of_save': - if (!state.itemKey) { - await i.reply({ content: '❌ Falta itemKey en Base.', flags: MessageFlags.Ephemeral }); - return; - } - const it = await prisma.economyItem.findFirst({ where: { key: state.itemKey, OR: [{ guildId }, { guildId: null }] }, orderBy: [{ guildId: 'desc' }] }); - if (!it) { - await i.reply({ content: '❌ Item no encontrado por key.', flags: MessageFlags.Ephemeral }); - return; - } - try { - await prisma.shopOffer.update({ - where: { id: state.offerId }, - data: { - itemId: it.id, - enabled: state.enabled ?? true, - price: state.price ?? {}, - startAt: state.startAt ? new Date(state.startAt) : null, - endAt: state.endAt ? new Date(state.endAt) : null, - perUserLimit: state.perUserLimit ?? null, - stock: state.stock ?? null, - metadata: state.metadata ?? {}, - } - }); - await i.reply({ content: '✅ Oferta actualizada.', flags: MessageFlags.Ephemeral }); - await editorMsg.edit({ - content: null, - flags: 32768, - components: [{ - type: 17, - accent_color: 0x00FF00, - components: [{ - type: 10, - content: `✅ **Oferta \`${state.offerId}\` actualizada.**` - }] - }] - }); - collector.stop('saved'); - } catch (err: any) { - await i.reply({ content: `❌ Error al guardar: ${err?.message ?? err}`, flags: MessageFlags.Ephemeral }); - } - return; - } - } catch (e) { - if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral }); - } - }); - - collector.on('end', async (_c: any, reason: string) => { - if (reason === 'time') { - try { - await editorMsg.edit({ - content: null, - flags: 32768, - components: [{ - type: 17, - accent_color: 0xFFA500, - components: [{ - type: 10, - content: '**⏰ Editor expirado.**' - }] - }] - }); - } catch {} - } - }); - } -}; - -async function showBaseModal(i: ButtonInteraction, state: OfferState, editorMsg: Message, buildComponents: () => any[]) { - const modal = { title: 'Base de Oferta', customId: 'of_base_modal', components: [ - { type: ComponentType.Label, label: 'Item Key', component: { type: ComponentType.TextInput, customId: 'itemKey', style: TextInputStyle.Short, required: true, value: state.itemKey ?? '' } }, - { type: ComponentType.Label, label: 'Habilitada? (true/false)', component: { type: ComponentType.TextInput, customId: 'enabled', style: TextInputStyle.Short, required: false, value: String(state.enabled ?? true) } }, - ] } as const; - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - state.itemKey = sub.components.getTextInputValue('itemKey').trim(); - const en = sub.components.getTextInputValue('enabled').trim(); - state.enabled = en ? (en.toLowerCase() !== 'false') : (state.enabled ?? true); - await sub.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents() - }); - } catch {} -} - -async function showJsonModal(i: ButtonInteraction, state: OfferState, field: 'price'|'metadata', title: string, editorMsg: Message, buildComponents: () => any[]) { - const current = JSON.stringify(state[field] ?? {}); - const modal = { title, customId: `of_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.deferUpdate(); - } catch { - await sub.reply({ content: '❌ JSON inválido.', flags: MessageFlags.Ephemeral }); - return; - } - } else { - state[field] = {}; - await sub.deferUpdate(); - } - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents() - }); - } catch {} -} - -async function showWindowModal(i: ButtonInteraction, state: OfferState, editorMsg: Message, buildComponents: () => any[]) { - const modal = { title: 'Ventana', customId: 'of_window_modal', components: [ - { type: ComponentType.Label, label: 'Inicio (ISO opcional)', component: { type: ComponentType.TextInput, customId: 'start', style: TextInputStyle.Short, required: false, value: state.startAt ?? '' } }, - { type: ComponentType.Label, label: 'Fin (ISO opcional)', component: { type: ComponentType.TextInput, customId: 'end', style: TextInputStyle.Short, required: false, value: state.endAt ?? '' } }, - ] } as const; - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - const s = sub.components.getTextInputValue('start').trim(); - const e = sub.components.getTextInputValue('end').trim(); - state.startAt = s || ''; - state.endAt = e || ''; - await sub.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents() - }); - } catch {} -} - -async function showLimitsModal(i: ButtonInteraction, state: OfferState, editorMsg: Message, buildComponents: () => any[]) { - const modal = { title: 'Límites', customId: 'of_limits_modal', components: [ - { type: ComponentType.Label, label: 'Límite por usuario (vacío = sin límite)', component: { type: ComponentType.TextInput, customId: 'limit', style: TextInputStyle.Short, required: false, value: state.perUserLimit != null ? String(state.perUserLimit) : '' } }, - { type: ComponentType.Label, label: 'Stock global (vacío = ilimitado)', component: { type: ComponentType.TextInput, customId: 'stock', style: TextInputStyle.Short, required: false, value: state.stock != null ? String(state.stock) : '' } }, - ] } as const; - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - const lim = sub.components.getTextInputValue('limit').trim(); - const st = sub.components.getTextInputValue('stock').trim(); - state.perUserLimit = lim ? Math.max(0, parseInt(lim, 10) || 0) : null; - state.stock = st ? Math.max(0, parseInt(st, 10) || 0) : null; - await sub.deferUpdate(); - await editorMsg.edit({ - content: null, - flags: 32768, - components: buildComponents() - }); - } catch {} -} diff --git a/src/.backup/game/pelear.ts b/src/.backup/game/pelear.ts deleted file mode 100644 index be0a4e5..0000000 --- a/src/.backup/game/pelear.ts +++ /dev/null @@ -1,256 +0,0 @@ -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 { formatToolLabel, combatSummaryRPG } from "../../../game/lib/rpgFormat"; -import { buildAreaMetadataBlocks } from "./_helpers"; - -const FIGHT_ACCENT = 0x992d22; - -export const command: CommandMessage = { - name: "pelear", - type: "message", - aliases: ["fight", "arena"], - cooldown: 8, - category: "Minijuegos", - 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; - const { levelArg, providedTool, areaOverride } = parseGameArgs(args); - - const areaInfo = areaOverride - ? await resolveGuildAreaWithFallback(guildId, areaOverride) - : await resolveAreaByType(guildId, "FIGHT"); - - if (!areaInfo.area) { - if (areaOverride) { - 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:`." - ); - } - 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 level = levelArg ?? (await getDefaultLevel(userId, guildId, area.id)); - const toolKey = - providedTool ?? (await findBestToolKey(userId, guildId, "sword")); - - try { - // Pre-check: si el nivel tiene requirements con herramienta obligatoria - // intentamos validar rápidamente si existe alguna herramienta equipada o en inventario - // usando validateRequirements indirectamente sería duplicar lógica; hacemos una ligera comprobación - // basada en `toolKey` detectado y si findBestToolKey falla, informaremos. - 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) - ) - .map((r) => r.itemKey!); - if (result.tool?.key) rewardKeys.push(result.tool.key); - if (result.weaponTool?.key) rewardKeys.push(result.weaponTool.key); - const rewardItems = await fetchItemBasics(guildId, rewardKeys); - - // Actualizar stats y misiones - await updateStats(userId, guildId, { fightsCompleted: 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 - ); - } - - const newAchievements = await checkAchievements( - userId, - guildId, - "fight_count" - ); - - let 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") - : "• —"; - if (result.rewardModifiers?.baseCoinsAwarded != null) { - const { baseCoinsAwarded, coinsAfterPenalty, fatigueCoinMultiplier } = - result.rewardModifiers; - if ( - fatigueCoinMultiplier != null && - fatigueCoinMultiplier < 1 && - baseCoinsAwarded != null && - coinsAfterPenalty != null - ) { - const pct = Math.round((1 - fatigueCoinMultiplier) * 100); - rewardLines += `\n (⚠️ Fatiga: monedas base ${baseCoinsAwarded} → ${coinsAfterPenalty} (-${pct}%) )`; - } - } - const mobsLines = result.mobs.length - ? result.mobs.map((m) => `• ${m}`).join("\n") - : "• —"; - const toolInfo = result.tool?.key - ? formatToolLabel({ - key: result.tool.key, - displayName: formatItemLabel( - rewardItems.get(result.tool.key) ?? { - key: result.tool.key, - name: null, - icon: null, - }, - { fallbackIcon: "🗡️" } - ), - instancesRemaining: result.tool.instancesRemaining, - broken: result.tool.broken, - brokenInstance: result.tool.brokenInstance, - durabilityDelta: result.tool.durabilityDelta, - remaining: result.tool.remaining, - max: result.tool.max, - source: result.tool.toolSource, - }) - : "—"; - const combatSummary = result.combat - ? combatSummaryRPG({ - mobs: result.mobs.length, - mobsDefeated: result.combat.mobsDefeated, - totalDamageDealt: result.combat.totalDamageDealt, - totalDamageTaken: result.combat.totalDamageTaken, - playerStartHp: result.combat.playerStartHp, - playerEndHp: result.combat.playerEndHp, - outcome: result.combat.outcome, - }) - : null; - - const blocks = [textBlock("# ⚔️ Arena")]; - - if (globalNotice) { - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - blocks.push(textBlock(globalNotice)); - } - - 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}` - ) - ); - 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}`)); - if (combatSummary) { - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - blocks.push(textBlock(combatSummary)); - } - - // 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"); - blocks.push(textBlock(`🏆 ¡Logro desbloqueado!\n${achLines}`)); - } - - const display = buildDisplay(FIGHT_ACCENT, blocks); - await sendDisplayReply(message, display); - } catch (e: any) { - const msg = (e?.message || String(e)).toLowerCase(); - // Mapear errores conocidos a mensajes más amigables - if ( - msg.includes("area no encontrada") || - msg.includes("nivel no encontrado") - ) { - await message.reply( - "⚠️ El área o nivel especificado no existe para este servidor." - ); - return; - } - if (msg.includes("cooldown activo")) { - await message.reply( - "⏳ Estás en cooldown para esta actividad. Intenta más tarde." - ); - return; - } - if ( - msg.includes("se requiere una herramienta adecuada") || - msg.includes("no tienes la herramienta") || - msg.includes("tipo de herramienta incorrecto") || - msg.includes("tier de herramienta insuficiente") - ) { - // Mensaje más específico: si no hay arma equipada y el área requiere saberlo, sugerir equipar o conseguir herramienta - await message.reply( - "⚠️ No tienes una herramienta válida para esta actividad. Equipa una herramienta adecuada (ej: espada) o especifica `toolKey`." - ); - return; - } - if ( - msg.includes("no puede infligir daño") || - msg.includes("autoDefeatNoWeapon") || - msg.includes("auto defeat") - ) { - await message.reply( - "⚠️ No tienes un arma equipada o válida para pelear. Equipa un arma para poder infligir daño o usa `pelear ` con una herramienta válida." - ); - return; - } - // Fallback genérico - await message.reply(`❌ No se pudo pelear: ${e?.message ?? e}`); - } - }, -}; diff --git a/src/.backup/game/pescar.ts b/src/.backup/game/pescar.ts deleted file mode 100644 index 22076ff..0000000 --- a/src/.backup/game/pescar.ts +++ /dev/null @@ -1,222 +0,0 @@ -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 { formatToolLabel, combatSummaryRPG } from "../../../game/lib/rpgFormat"; -import { buildAreaMetadataBlocks } from "./_helpers"; - -const FISHING_ACCENT = 0x1abc9c; - -export const command: CommandMessage = { - 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)", - run: async (message, args, _client: Amayo) => { - const userId = message.author.id; - const guildId = message.guild!.id; - const { levelArg, providedTool, areaOverride } = parseGameArgs(args); - - const areaInfo = areaOverride - ? await resolveGuildAreaWithFallback(guildId, areaOverride) - : await resolveAreaByType(guildId, "LAGOON"); - - if (!areaInfo.area) { - if (areaOverride) { - 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:`." - ); - } - 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 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 rewardKeys = result.rewards - .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); - if (result.weaponTool?.key) rewardKeys.push(result.weaponTool.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" - ); - - let 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") - : "• —"; - if (result.rewardModifiers?.baseCoinsAwarded != null) { - const { baseCoinsAwarded, coinsAfterPenalty, fatigueCoinMultiplier } = - result.rewardModifiers; - if ( - fatigueCoinMultiplier != null && - fatigueCoinMultiplier < 1 && - baseCoinsAwarded != null && - coinsAfterPenalty != null - ) { - const pct = Math.round((1 - fatigueCoinMultiplier) * 100); - rewardLines += `\n (⚠️ Fatiga: monedas base ${baseCoinsAwarded} → ${coinsAfterPenalty} (-${pct}%) )`; - } - } - const mobsLines = result.mobs.length - ? result.mobs.map((m) => `• ${m}`).join("\n") - : "• —"; - const toolInfo = result.tool?.key - ? formatToolLabel({ - key: result.tool.key, - displayName: formatItemLabel( - rewardItems.get(result.tool.key) ?? { - key: result.tool.key, - name: null, - icon: null, - }, - { fallbackIcon: "🎣" } - ), - instancesRemaining: result.tool.instancesRemaining, - broken: result.tool.broken, - brokenInstance: result.tool.brokenInstance, - durabilityDelta: result.tool.durabilityDelta, - remaining: result.tool.remaining, - max: result.tool.max, - source: result.tool.toolSource, - }) - : "—"; - - const weaponInfo = result.weaponTool?.key - ? formatToolLabel({ - key: result.weaponTool.key, - displayName: formatItemLabel( - rewardItems.get(result.weaponTool.key) ?? { - key: result.weaponTool.key, - name: null, - icon: null, - }, - { fallbackIcon: "⚔️" } - ), - instancesRemaining: result.weaponTool.instancesRemaining, - broken: result.weaponTool.broken, - brokenInstance: result.weaponTool.brokenInstance, - durabilityDelta: result.weaponTool.durabilityDelta, - remaining: result.weaponTool.remaining, - max: result.weaponTool.max, - source: result.weaponTool.toolSource, - }) - : null; - const combatSummary = result.combat - ? combatSummaryRPG({ - mobs: result.mobs.length, - mobsDefeated: result.combat.mobsDefeated, - totalDamageDealt: result.combat.totalDamageDealt, - totalDamageTaken: result.combat.totalDamageTaken, - playerStartHp: result.combat.playerStartHp, - playerEndHp: result.combat.playerEndHp, - outcome: result.combat.outcome, - }) - : null; - - const blocks = [textBlock("# 🎣 Pesca")]; - - if (globalNotice) { - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - blocks.push(textBlock(globalNotice)); - } - - blocks.push(dividerBlock()); - const areaScope = - source === "global" - ? "🌐 Configuración global" - : "📍 Configuración local"; - const toolsLine = weaponInfo - ? `**Caña:** ${toolInfo}\n**Arma (defensa):** ${weaponInfo}` - : `**Herramienta:** ${toolInfo}`; - blocks.push( - textBlock( - `**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n${toolsLine}` - ) - ); - 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}`)); - if (combatSummary) { - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - blocks.push(textBlock(combatSummary)); - } - - // 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"); - blocks.push(textBlock(`🏆 ¡Logro desbloqueado!\n${achLines}`)); - } - - const display = buildDisplay(FISHING_ACCENT, blocks); - await sendDisplayReply(message, display); - } catch (e: any) { - await message.reply(`❌ No se pudo pescar: ${e?.message ?? e}`); - } - }, -}; diff --git a/src/.backup/game/plantar.ts b/src/.backup/game/plantar.ts deleted file mode 100644 index fd80bb4..0000000 --- a/src/.backup/game/plantar.ts +++ /dev/null @@ -1,119 +0,0 @@ -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; - -export const command: CommandMessage = { - 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)", - run: async (message, args, _client: Amayo) => { - const userId = message.author.id; - const guildId = message.guild!.id; - 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; - } - - 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")); - - try { - 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) - ) - .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") - : "• —"; - const mobsLines = result.mobs.length - ? result.mobs.map((m) => `• ${m}`).join("\n") - : "• —"; - const toolInfo = result.tool?.key - ? `${formatItemLabel( - rewardItems.get(result.tool.key) ?? { - key: result.tool.key, - name: null, - icon: null, - }, - { fallbackIcon: "🪓" } - )}${ - result.tool.broken - ? " (rota)" - : ` (-${result.tool.durabilityDelta ?? 0} dur.)` - }` - : "—"; - - const blocks = [ - textBlock("# 🌱 Campo"), - dividerBlock(), - textBlock( - `**Área:** \`${area.key}\`\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}` - ), - dividerBlock({ divider: false, spacing: 1 }), - textBlock(`**Recompensas**\n${rewardLines}`), - dividerBlock({ divider: false, spacing: 1 }), - textBlock(`**Eventos**\n${mobsLines}`), - ]; - - // Añadir metadata del área - const metaBlocks = buildAreaMetadataBlocks(area); - if (metaBlocks.length) { - blocks.push(dividerBlock()); - // @ts-ignore: extended block type allowed at runtime - blocks.push(...metaBlocks); - } - - const display = buildDisplay(FARM_ACCENT, blocks); - await sendDisplayReply(message, display); - } catch (e: any) { - await message.reply(`❌ No se pudo plantar: ${e?.message ?? e}`); - } - }, -}; diff --git a/src/.backup/game/player.ts b/src/.backup/game/player.ts deleted file mode 100644 index 10ed905..0000000 --- a/src/.backup/game/player.ts +++ /dev/null @@ -1,229 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { prisma } from "../../../core/database/prisma"; -import { getOrCreateWallet } from "../../../game/economy/service"; -import { - getEquipment, - getEffectiveStats, -} from "../../../game/combat/equipmentService"; -import { - getPlayerStatsFormatted, - getOrCreatePlayerStats, -} from "../../../game/stats/service"; -import type { TextBasedChannel } from "discord.js"; -import { formatItemLabel } from "./_helpers"; -import { heartsBar } from "../../../game/lib/rpgFormat"; -import { getActiveStatusEffects } from "../../../game/combat/statusEffectsService"; - -export const command: CommandMessage = { - name: "player", - type: "message", - aliases: ["perfil", "profile", "yo", "me"], - cooldown: 5, - category: "Economía", - description: - "Muestra toda tu información de jugador con vista visual mejorada", - usage: "player [@usuario]", - run: async (message, args, _client: Amayo) => { - const targetUser = message.mentions.users.first() || message.author; - const userId = targetUser.id; - const guildId = message.guild!.id; - - // Obtener datos del jugador - const wallet = await getOrCreateWallet(userId, guildId); - const { eq, weapon, armor, cape } = await getEquipment(userId, guildId); - const stats = await getEffectiveStats(userId, guildId); - const showDefense = - stats.baseDefense != null && stats.baseDefense !== stats.defense - ? `${stats.defense} (_${stats.baseDefense}_ base)` - : `${stats.defense}`; - const showDamage = - stats.baseDamage != null && stats.baseDamage !== stats.damage - ? `${stats.damage} (_${stats.baseDamage}_ base)` - : `${stats.damage}`; - const playerStats = await getPlayerStatsFormatted(userId, guildId); - const rawStats = await getOrCreatePlayerStats(userId, guildId); - const streak = rawStats.currentWinStreak; - const streakBonusPct = Math.min(Math.floor(streak / 3), 30); // cada 3 = 1%, mostramos valor base en % - const damageBonusDisplay = - streakBonusPct > 0 ? `(+${streakBonusPct}% racha)` : ""; - const effects = await getActiveStatusEffects(userId, guildId); - - // Progreso por áreas - const progress = await prisma.playerProgress.findMany({ - where: { userId, guildId }, - include: { area: true }, - orderBy: { updatedAt: "desc" }, - take: 5, - }); - - // Inventario - const inventoryCount = await prisma.inventoryEntry.count({ - where: { userId, guildId, quantity: { gt: 0 } }, - }); - - const inventorySum = await prisma.inventoryEntry.aggregate({ - where: { userId, guildId }, - _sum: { quantity: true }, - }); - - // Cooldowns activos - const activeCooldowns = await prisma.actionCooldown.findMany({ - where: { userId, guildId, until: { gt: new Date() } }, - orderBy: { until: "asc" }, - take: 3, - }); - - const weaponLine = weapon - ? `⚔️ Arma: ${formatItemLabel(weapon, { - fallbackIcon: "🗡️", - bold: true, - })}` - : "⚔️ Arma: *Ninguna*"; - const armorLine = armor - ? `🛡️ Armadura: ${formatItemLabel(armor, { - fallbackIcon: "🛡️", - bold: true, - })}` - : "🛡️ Armadura: *Ninguna*"; - const capeLine = cape - ? `🧥 Capa: ${formatItemLabel(cape, { fallbackIcon: "🧥", bold: true })}` - : "🧥 Capa: *Ninguna*"; - - // Crear DisplayComponent - const display = { - type: 17, - accent_color: 0x5865f2, - components: [ - { - type: 10, - content: `👤 **${targetUser.username}**\n${ - targetUser.bot ? "🤖 Bot" : "👨 Usuario" - }`, - }, - { type: 14, divider: true }, - { - type: 10, - content: - `**<:stats:1425689271788113991> ESTADÍSTICAS**\n` + - `<:healbonus:1425671499792121877> HP: **${stats.hp}/${ - stats.maxHp - }** ${heartsBar(stats.hp, stats.maxHp)}\n` + - `<:damage:1425670476449189998> ATK: **${showDamage}** ${damageBonusDisplay}\n` + - `<:defens:1425670433910427862> DEF: **${showDefense}**\n` + - `🏆 Racha: **${streak}** (mejor: ${rawStats.longestWinStreak})\n` + - ` Monedas: **${wallet.coins.toLocaleString()}**`, - }, - { type: 14, divider: true }, - { - type: 10, - content: - `**<:damage:1425670476449189998> EQUIPO**\n` + - `${weaponLine}\n` + - `${armorLine}\n` + - `${capeLine}`, - }, - { type: 14, divider: true }, - { - type: 10, - content: - `**🎒 INVENTARIO**\n` + - `<:emptybox:1425678700753588305> Items únicos: **${inventoryCount}**\n` + - `<:table:1425673712312782879> Total items: **${ - inventorySum._sum.quantity ?? 0 - }**`, - }, - ], - }; - - // Añadir efectos activos (después de construir el bloque base para mantener orden) - if (effects.length > 0) { - const nowTs = Date.now(); - const fxLines = effects - .map((e) => { - let remain = ""; - if (e.expiresAt) { - const ms = e.expiresAt.getTime() - nowTs; - if (ms > 0) { - const m = Math.floor(ms / 60000); - const s = Math.floor((ms % 60000) / 1000); - remain = ` (${m}m ${s}s)`; - } else remain = " (exp)"; - } - switch (e.type) { - case "FATIGUE": { - const pct = Math.round(e.magnitude * 100); - return `• Fatiga: -${pct}% daño${remain}`; - } - default: - return `• ${e.type}${remain}`; - } - }) - .join("\n"); - display.components.push({ type: 14, divider: true }); - display.components.push({ - type: 10, - content: `**😵 EFECTOS ACTIVOS**\n${fxLines}`, - }); - } - - // Añadir stats de actividades si existen - if (playerStats.activities) { - const activitiesText = Object.entries(playerStats.activities) - .filter(([_, value]) => value > 0) - .map(([key, value]) => `${key}: **${value}**`) - .join("\n"); - - if (activitiesText) { - display.components.push({ type: 14, divider: true }); - display.components.push({ - type: 10, - content: `**🎮 ACTIVIDADES**\n${activitiesText}`, - }); - } - } - - // Añadir progreso por áreas - if (progress.length > 0) { - display.components.push({ type: 14, divider: true }); - display.components.push({ - type: 10, - content: - `**🗺️ PROGRESO EN ÁREAS**\n` + - progress - .map( - (p) => - `• ${p.area.name || p.area.key}: Nivel **${p.highestLevel}**` - ) - .join("\n"), - }); - } - - // Añadir cooldowns activos - if (activeCooldowns.length > 0) { - const now = Date.now(); - const cooldownsText = activeCooldowns - .map((cd) => { - const remaining = Math.ceil((cd.until.getTime() - now) / 1000); - const mins = Math.floor(remaining / 60); - const secs = remaining % 60; - return `• ${cd.key}: **${mins}m ${secs}s**`; - }) - .join("\n"); - - display.components.push({ type: 14, divider: true }); - display.components.push({ - type: 10, - content: `**<:swordcooldown:1425695375028912168> COOLDOWNS ACTIVOS**\n${cooldownsText}`, - }); - } - - const channel = message.channel as TextBasedChannel & { send: Function }; - await (channel.send as any)({ - content: null, - components: [display], - flags: 32768, // MessageFlags.IS_COMPONENTS_V2 - reply: { messageReference: message.id }, - }); - }, -}; diff --git a/src/.backup/game/racha.ts b/src/.backup/game/racha.ts deleted file mode 100644 index 68df239..0000000 --- a/src/.backup/game/racha.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { getStreakInfo, updateStreak } from "../../../game/streaks/service"; -import type { TextBasedChannel } from "discord.js"; -import { fetchItemBasics, formatItemLabel, sendDisplayReply } from "./_helpers"; -import { - buildDisplay, - textBlock, - dividerBlock, -} from "../../../core/lib/componentsV2"; - -export const command: CommandMessage = { - name: "racha", - type: "message", - aliases: ["streak", "daily"], - category: "Economía", - cooldown: 10, - description: "Ver tu racha diaria y reclamar recompensa", - usage: "racha", - run: async (message, args, client: Amayo) => { - try { - const userId = message.author.id; - const guildId = message.guild!.id; - - // Actualizar racha - const { streak, newDay, rewards, daysIncreased } = await updateStreak( - userId, - guildId - ); - - // Construir bloques de display (evitando type:9 sin accessory) - const blocks: any[] = [ - textBlock( - `## Racha diaria de ${message.author.username}` - ), - dividerBlock({ divider: false, spacing: 1 }), - textBlock( - `**<:stats:1425689271788113991> ESTADÍSTICAS**\n` + - ` Racha Actual: **${streak.currentStreak}** días\n` + - ` Mejor Racha: **${streak.longestStreak}** días\n` + - `<:events:1425691310194561106> Días Activos: **${streak.totalDaysActive}** días` - ), - dividerBlock({ spacing: 1, divider: false }), - ]; - - // Mensaje de estado - if (newDay) { - if (daysIncreased) { - blocks.push( - textBlock( - `**<:Sup_res:1420535051162095747> ¡RACHA INCREMENTADA!**\nHas mantenido tu racha por **${streak.currentStreak}** días seguidos.` - ) - ); - } else { - blocks.push( - textBlock( - `**<:Sup_urg:1420535068056748042> RACHA REINICIADA**\nPasó más de un día sin actividad. Tu racha se ha reiniciado.` - ) - ); - } - - // Mostrar recompensas - if (rewards) { - let rewardsText = - "** RECOMPENSA DEL DÍA**\n"; - if (rewards.coins) - rewardsText += `<:coin:1425667511013081169> **${rewards.coins.toLocaleString()}** monedas\n`; - if (rewards.items && rewards.items.length) { - const basics = await fetchItemBasics( - guildId, - rewards.items.map((item) => item.key) - ); - rewards.items.forEach((item) => { - const info = basics.get(item.key) ?? { - key: item.key, - name: null, - icon: null, - }; - const label = formatItemLabel(info, { bold: true }); - rewardsText += `${label} ×${item.quantity}\n`; - }); - } - - blocks.push(dividerBlock({ spacing: 1, divider: false })); - blocks.push(textBlock(rewardsText)); - } - } else { - blocks.push( - textBlock( - `**<:apin:1336533845541126174> YA RECLAMASTE HOY**\nYa has reclamado tu recompensa diaria. Vuelve mañana para continuar tu racha.` - ) - ); - } - - // Próximos hitos - const milestones = [3, 7, 14, 30, 60, 90, 180, 365]; - const nextMilestone = milestones.find((m) => m > streak.currentStreak); - - if (nextMilestone) { - const remaining = nextMilestone - streak.currentStreak; - blocks.push(dividerBlock({ spacing: 1, divider: false })); - blocks.push( - textBlock( - `**🎯 PRÓXIMO HITO**\nFaltan **${remaining}** días para alcanzar el día **${nextMilestone}**` - ) - ); - } - - const display = buildDisplay(daysIncreased ? 0x00ff00 : 0xffa500, blocks); - - await sendDisplayReply(message, display); - } catch (error) { - console.error("Error en comando racha:", error); - await message.reply( - "<:Cross:1420535096208920576> Error al obtener tu racha diaria." - ); - } - }, -}; diff --git a/src/.backup/game/setup.ts b/src/.backup/game/setup.ts deleted file mode 100644 index 2c9843a..0000000 --- a/src/.backup/game/setup.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Message, TextBasedChannel } from "discord.js"; -import type { CommandMessage } from "../../../core/types/commands"; -import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; -import fs from "fs"; -import path from "path"; -import logger from "../../../core/lib/logger"; -import type Amayo from "../../../core/client"; - -// Helper: split text into chunks under Discord message limit (~2000) -function chunkText(text: string, size = 1900) { - const parts: string[] = []; - let i = 0; - while (i < text.length) { - parts.push(text.slice(i, i + size)); - i += size; - } - return parts; -} - -export const command: CommandMessage = { - name: "setup", - type: "message", - aliases: ["setup-ejemplos", "setup-demo"], - cooldown: 10, - description: - "Publica ejemplos básicos y avanzados para configurar items, mobs y áreas.", - category: "Admin", - usage: "setup [advanced]", - run: async (message: Message, args: string[], client: Amayo) => { - const channel = message.channel as TextBasedChannel & { send: Function }; - const allowed = await hasManageGuildOrStaff( - message.member, - message.guild!.id, - client.prisma - ); - if (!allowed) { - await channel.send({ - content: "❌ No tienes permisos de ManageGuild ni rol de staff.", - }); - return; - } - - const doInitFull = args[0] === "init-full" || args.includes("init-full"); - if (doInitFull) { - await channel.send( - "Iniciando FULL setup: creando items, areas, mobs y recetas (modo idempotente). Esto puede tardar unos segundos." - ); - try { - const setupMod: any = await import( - "../../../../scripts/fullServerSetup.js" - ); - if (typeof setupMod.runFullServerSetup === "function") { - // Use guild id from the current guild context - await setupMod.runFullServerSetup(message.guild!.id); - await channel.send("✅ Full setup completado."); - } else { - await channel.send( - "❌ El módulo de setup completo no exporta runFullServerSetup()." - ); - } - } catch (e) { - logger.error({ e }, "setup init-full failed"); - await channel.send( - `❌ Error corriendo fullServerSetup: ${ - (e && (e as any).message) || e - }` - ); - } - return; - } - }, -}; diff --git a/src/.backup/game/stats.ts b/src/.backup/game/stats.ts deleted file mode 100644 index 3812578..0000000 --- a/src/.backup/game/stats.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { getPlayerStatsFormatted } from "../../../game/stats/service"; -import type { TextBasedChannel } from "discord.js"; - -export const command: CommandMessage = { - name: "stats", - type: "message", - aliases: ["estadisticas", "est"], - cooldown: 5, - category: "Economía", - description: "Ver estadísticas detalladas de un jugador", - usage: "stats [@usuario]", - run: async (message, args, client: Amayo) => { - try { - const guildId = message.guild!.id; - const targetUser = message.mentions.users.first() || message.author; - const userId = targetUser.id; - - // Obtener estadísticas formateadas - const stats = await getPlayerStatsFormatted(userId, guildId); - - const formatValue = (value: unknown): string => { - if (typeof value === "number") return value.toLocaleString(); - if (typeof value === "bigint") return value.toString(); - if (typeof value === "string") return value.trim() || "0"; - return value == null ? "0" : String(value); - }; - - const components: any[] = [ - { - type: 10, - content: `## <:stats:1425689271788113991> Estadísticas de ${targetUser.username}`, - }, - { type: 14, divider: false, spacing: 1 }, - ]; - - const addSection = (title: string, data?: Record) => { - if (!data || typeof data !== "object") return; - const entries = Object.entries(data); - const lines = entries.map( - ([key, value]) => `${key}: **${formatValue(value)}**` - ); - const content = lines.length > 0 ? lines.join("\n") : "Sin datos"; - components.push({ - type: 10, - content: `**${title}**\n${content}`, - }); - components.push({ type: 14, divider: false, spacing: 1 }); - }; - - addSection( - "<:stats:1425689271788113991> ACTIVIDADES", - stats.activities as Record | undefined - ); - addSection( - "<:damage:1425670476449189998> COMBATE", - stats.combat as Record | undefined - ); - addSection( - "<:coin:1425667511013081169> ECONOMÍA", - stats.economy as Record | undefined - ); - addSection( - "<:emptybox:1425678700753588305> ITEMS", - stats.items as Record | undefined - ); - addSection( - " RÉCORDS", - stats.records as Record | undefined - ); - - // Remove trailing separator if present - if ( - components.length > 0 && - components[components.length - 1]?.type === 14 - ) { - components.pop(); - } - - if (components.length === 1) { - components.push({ - type: 10, - content: "*Sin estadísticas registradas.*", - }); - } - - // Crear DisplayComponent - const display = { - type: 17, - accent_color: 0x5865f2, - components, - }; - - // Enviar con flags - const channel = message.channel as TextBasedChannel & { send: Function }; - await (channel.send as any)({ - content: null, - components: [display], - flags: 32768, // MessageFlags.IS_COMPONENTS_V2 - reply: { messageReference: message.id }, - }); - } catch (error) { - console.error("Error en comando stats:", error); - await message.reply( - "<:Cross:1420535096208920576> Error al obtener las estadísticas." - ); - } - }, -}; diff --git a/src/.backup/game/tienda.ts b/src/.backup/game/tienda.ts deleted file mode 100644 index 6725dc8..0000000 --- a/src/.backup/game/tienda.ts +++ /dev/null @@ -1,594 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { - Message, - ButtonInteraction, - MessageComponentInteraction, - ComponentType, - ButtonStyle, - MessageFlags, - StringSelectMenuInteraction, - email, -} from "discord.js"; -import { prisma } from "../../../core/database/prisma"; -import { getOrCreateWallet, buyFromOffer } from "../../../game/economy/service"; -import type { DisplayComponentContainer } from "../../../core/types/displayComponents"; -import type { ItemProps } from "../../../game/economy/types"; -import { formatItemLabel, resolveItemIcon } from "./_helpers"; - -const ITEMS_PER_PAGE = 5; - -// Helper para convertir cadena como <:name:id> o en objeto emoji válido -function buildEmoji( - raw: string | undefined -): { id?: string; name: string; animated?: boolean } | undefined { - if (!raw) return undefined; - // Si viene ya sin brackets retornar como nombre simple - if (!raw.startsWith("<") || !raw.endsWith(">")) { - return { name: raw }; - } - // Formatos: o <:name:id> - const match = raw.match(/^<(a?):([^:>]+):([0-9]+)>$/); - if (!match) return undefined; - const animated = match[1] === "a"; - const name = match[2]; - const id = match[3]; - return { id, name, animated }; -} - -import { parseItemProps } from "../../../game/core/utils"; - -function formatPrice(price: any): string { - const parts: string[] = []; - if (price.coins) parts.push(`<:coin:1425667511013081169> ${price.coins}`); - if (price.items && price.items.length > 0) { - for (const item of price.items) { - parts.push(`📦 ${item.itemKey} x${item.qty}`); - } - } - return parts.join(" + ") || "<:free:1425681948172357732>"; -} - -function getItemIcon(props: ItemProps, category?: string): string { - if (props.tool) { - const t = props.tool.type; - if (t === "pickaxe") return "<:pickaxe_default:1424589544585695398>"; - if (t === "rod") return "<:rod:1425680136912633866>"; - if (t === "sword") return "<:27621stonesword:1424591948102107167>"; - if (t === "bow") return "<:bow:1425680803756511232>"; - if (t === "halberd") return "<:hgard:1425681197316571217>"; - if (t === "net") return "<:net:1425681511788576839>"; - return "<:table:1425673712312782879>"; - } - if (props.damage && props.damage > 0) return "<:damage:1425670476449189998>"; - if (props.defense && props.defense > 0) - return "<:defens:1425670433910427862>"; - if (props.food) return "<:clipmushroom:1425679121954115704>"; - if (props.chest) return "<:legendchest:1425679137565179914>"; - if (category === "consumables") return "🧪"; - if (category === "materials") return "🔨"; - return "<:emptybox:1425678700753588305>"; -} - -export const command: CommandMessage = { - name: "tienda", - type: "message", - aliases: ["shop", "store"], - cooldown: 5, - category: "Economía", - description: - "Abre la tienda y navega por las ofertas disponibles con un panel interactivo.", - usage: "tienda [categoria]", - run: async (message, args, _client: Amayo) => { - const userId = message.author.id; - const guildId = message.guild!.id; - - // Obtener wallet del usuario - const wallet = await getOrCreateWallet(userId, guildId); - - // Obtener todas las ofertas activas - const now = new Date(); - const offers = await prisma.shopOffer.findMany({ - where: { - guildId, - enabled: true, - OR: [ - { startAt: null, endAt: null }, - { startAt: { lte: now }, endAt: { gte: now } }, - { startAt: { lte: now }, endAt: null }, - { startAt: null, endAt: { gte: now } }, - ], - }, - include: { item: true }, - orderBy: { createdAt: "desc" }, - }); - - if (offers.length === 0) { - await message.reply( - " La tienda está vacía. ¡Vuelve más tarde!" - ); - return; - } - - // Filtrar por categoría si se proporciona - const categoryFilter = args[0]?.trim().toLowerCase(); - const filteredOffers = categoryFilter - ? offers.filter((o) => - o.item.category?.toLowerCase().includes(categoryFilter) - ) - : offers; - - if (filteredOffers.length === 0) { - await message.reply( - ` No hay ofertas en la categoría "${categoryFilter}".` - ); - return; - } - - // Estado inicial - const sessionState = { - currentPage: 1, - selectedOfferId: null as string | null, - }; - - const shopMessage = await message.reply({ - flags: MessageFlags.SuppressEmbeds | 32768, - components: await buildShopPanel( - filteredOffers, - sessionState.currentPage, - wallet.coins, - sessionState.selectedOfferId - ), - }); - - // Collector para interacciones - const collector = shopMessage.createMessageComponentCollector({ - time: 300000, // 5 minutos - filter: (i: MessageComponentInteraction) => - i.user.id === message.author.id, - }); - - collector.on( - "collect", - async (interaction: MessageComponentInteraction) => { - try { - if (interaction.isButton()) { - await handleButtonInteraction( - interaction as ButtonInteraction, - filteredOffers, - sessionState, - userId, - guildId, - shopMessage, - collector - ); - } else if (interaction.isStringSelectMenu()) { - await handleSelectInteraction( - interaction as StringSelectMenuInteraction, - filteredOffers, - sessionState.currentPage, - userId, - guildId, - shopMessage - ); - } - } catch (error: any) { - console.error("Error handling shop interaction:", error); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: `<:Cross:1420535096208920576> Error: ${ - error?.message ?? error - }`, - flags: MessageFlags.Ephemeral, - }); - } - } - } - ); - - collector.on("end", async (_, reason) => { - if (reason === "time") { - try { - await shopMessage.edit({ - components: await buildExpiredPanel(), - }); - } catch {} - } - }); - }, -}; - -async function buildShopPanel( - offers: any[], - page: number, - userCoins: number, - selectedOfferId: string | null -): Promise { - const totalPages = Math.ceil(offers.length / ITEMS_PER_PAGE); - const safePage = Math.min(Math.max(1, page), totalPages); - const start = (safePage - 1) * ITEMS_PER_PAGE; - const pageOffers = offers.slice(start, start + ITEMS_PER_PAGE); - - // Encontrar la oferta seleccionada - const selectedOffer = selectedOfferId - ? offers.find((o) => o.id === selectedOfferId) - : null; - - // Container principal - const container: DisplayComponentContainer = { - type: 17, - accent_color: 0xffa500, - components: [ - { - type: 10, - content: `### Tienda - Ofertas Disponibles`, - }, - { - type: 10, - content: `-# <:coin:1425667511013081169> Monedas: **${userCoins}**`, - }, - { - type: 14, - divider: false, - spacing: 2, - }, - ], - }; - - // Si hay una oferta seleccionada, mostrar detalles - if (selectedOffer) { - const item = selectedOffer.item; - const props = parseItemProps(item.props); - const label = formatItemLabel(item, { - fallbackIcon: getItemIcon(props, item.category), - bold: true, - }); - const price = formatPrice(selectedOffer.price); - - // Stock info - let stockInfo = ""; - if (selectedOffer.stock != null) { - stockInfo = `\n<:clipboard:1425669350316048435> Stock: ${selectedOffer.stock}`; - } - if (selectedOffer.perUserLimit != null) { - const purchased = await prisma.shopPurchase.aggregate({ - where: { offerId: selectedOffer.id }, - _sum: { qty: true }, - }); - const userPurchased = purchased._sum.qty ?? 0; - stockInfo += `\n<:Sup_urg:1420535068056748042> Límite por usuario: ${userPurchased}/${selectedOffer.perUserLimit}`; - } - - // Stats del item - let statsInfo = ""; - if (props.damage) - statsInfo += `\n<:damage:1425670476449189998> Daño: +${props.damage}`; - if (props.defense) - statsInfo += `\n<:defens:1425670433910427862> Defensa: +${props.defense}`; - if (props.maxHpBonus) - statsInfo += `\n<:healbonus:1425671499792121877> HP Bonus: +${props.maxHpBonus}`; - if (props.tool) - statsInfo += `\n<:table:1425673712312782879> Herramienta: ${ - props.tool.type - } T${props.tool.tier ?? 1}`; - if (props.food && props.food.healHp) - statsInfo += `\n<:cure:1425671519639572510> Cura: ${props.food.healHp} HP`; - - container.components.push({ - type: 10, - content: `${label}\n\n${ - item.description || "" - }${statsInfo}\n\nPrecio: ${price}${stockInfo}`, - }); - - container.components.push({ - type: 14, - divider: false, - spacing: 1, - }); - } - - // Lista de ofertas en la página - container.components.push({ - type: 10, - content: selectedOffer - ? "<:clipboard:1425669350316048435> **Otras Ofertas:**" - : "<:clipboard:1425669350316048435> **Ofertas Disponibles:**", - }); - - for (const offer of pageOffers) { - const item = offer.item; - const props = parseItemProps(item.props); - const label = formatItemLabel(item, { - fallbackIcon: getItemIcon(props, item.category), - bold: true, - }); - const price = formatPrice(offer.price); - const isSelected = selectedOfferId === offer.id; - - const stockText = - offer.stock != null ? ` (${offer.stock} disponibles)` : ""; - const selectedMark = isSelected ? " " : ""; - - container.components.push({ - type: 9, - components: [ - { - type: 10, - content: `${label}${selectedMark}\n${price}${stockText}`, - }, - ], - accessory: { - type: 2, - style: isSelected ? ButtonStyle.Success : ButtonStyle.Primary, - emoji: isSelected - ? buildEmoji("<:Sup_res:1420535051162095747>") - : buildEmoji("<:preview:1425678718918987976>"), - label: isSelected ? "Seleccionado" : "Ver", - custom_id: `shop_view_${offer.id}`, - }, - }); - } - - // Botones de navegación y acciones - const actionRow1 = { - type: ComponentType.ActionRow, - components: [ - { - type: ComponentType.Button, - style: ButtonStyle.Secondary, - //label: "<:blueskip2:1425682929782362122>", - emoji: buildEmoji("<:blueskip2:1425682929782362122>"), - custom_id: "shop_prev_page", - disabled: safePage <= 1, - }, - { - type: ComponentType.Button, - style: ButtonStyle.Secondary, - label: `${safePage}/${totalPages}`, - emoji: buildEmoji("<:apoint:1336536296767750298>"), - custom_id: "shop_current_page", - disabled: true, - }, - { - type: ComponentType.Button, - style: ButtonStyle.Secondary, - //label: "<:blueskip:1425682992801644627>", - emoji: buildEmoji("<:blueskip:1425682992801644627>"), - custom_id: "shop_next_page", - disabled: safePage >= totalPages, - }, - ], - }; - - const actionRow2 = { - type: ComponentType.ActionRow, - components: [ - { - type: ComponentType.Button, - style: ButtonStyle.Success, - label: "Comprar (x1)", - emoji: buildEmoji("<:onlineshopping:1425684275008897064>"), - custom_id: "shop_buy_1", - disabled: !selectedOfferId, - }, - { - type: ComponentType.Button, - style: ButtonStyle.Success, - label: "Comprar (x5)", - emoji: buildEmoji("<:onlineshopping:1425684275008897064>"), - custom_id: "shop_buy_5", - disabled: !selectedOfferId, - }, - { - type: ComponentType.Button, - style: ButtonStyle.Primary, - label: "Actualizar", - emoji: buildEmoji("<:reload:1425684687753580645>"), - custom_id: "shop_refresh", - }, - { - type: ComponentType.Button, - style: ButtonStyle.Danger, - label: "Cerrar", - emoji: buildEmoji("<:Cross:1420535096208920576>"), - custom_id: "shop_close", - }, - ], - }; - - return [container, actionRow1, actionRow2]; -} - -async function handleButtonInteraction( - interaction: ButtonInteraction, - offers: any[], - sessionState: { currentPage: number; selectedOfferId: string | null }, - userId: string, - guildId: string, - shopMessage: Message, - collector: any -): Promise { - const customId = interaction.customId; - - // Ver detalles de un item - if (customId.startsWith("shop_view_")) { - const offerId = customId.replace("shop_view_", ""); - const wallet = await getOrCreateWallet(userId, guildId); - // Toggle: si el usuario vuelve a pulsar la misma oferta, la des-selecciona para volver al listado general - if (sessionState.selectedOfferId === offerId) { - sessionState.selectedOfferId = null; - } else { - sessionState.selectedOfferId = offerId; - } - - await interaction.update({ - components: await buildShopPanel( - offers, - sessionState.currentPage, - wallet.coins, - sessionState.selectedOfferId - ), - }); - return; - } - - // Comprar - if (customId === "shop_buy_1" || customId === "shop_buy_5") { - const selectedOfferId = sessionState.selectedOfferId; - if (!selectedOfferId) { - await interaction.reply({ - content: "<:Cross:1420535096208920576> Primero selecciona un item.", - flags: MessageFlags.Ephemeral, - }); - return; - } - - const qty = customId === "shop_buy_1" ? 1 : 5; - - try { - await interaction.deferUpdate(); - const result = await buyFromOffer(userId, guildId, selectedOfferId, qty); - const wallet = await getOrCreateWallet(userId, guildId); - - const purchaseLabel = formatItemLabel(result.item, { - fallbackIcon: resolveItemIcon(result.item.icon), - }); - await interaction.followUp({ - content: `<:Sup_res:1420535051162095747> **Compra exitosa!**\n🛒 ${purchaseLabel} x${result.qty}\n<:coin:1425667511013081169> Te quedan: ${wallet.coins} monedas`, - flags: MessageFlags.Ephemeral, - }); - - // Actualizar tienda - await shopMessage.edit({ - components: await buildShopPanel( - offers, - sessionState.currentPage, - wallet.coins, - sessionState.selectedOfferId - ), - }); - } catch (error: any) { - await interaction.followUp({ - content: `<:Cross:1420535096208920576> No se pudo comprar: ${ - error?.message ?? error - }`, - flags: MessageFlags.Ephemeral, - }); - } - return; - } - - // Actualizar - if (customId === "shop_refresh") { - const wallet = await getOrCreateWallet(userId, guildId); - await interaction.update({ - components: await buildShopPanel( - offers, - sessionState.currentPage, - wallet.coins, - sessionState.selectedOfferId - ), - }); - return; - } - - // Cerrar - if (customId === "shop_close") { - await interaction.update({ - components: await buildClosedPanel(), - }); - collector.stop(); - return; - } - - // Navegación de páginas (ya manejado en el collect) - if (customId === "shop_prev_page" || customId === "shop_next_page") { - const wallet = await getOrCreateWallet(userId, guildId); - let newPage = sessionState.currentPage; - - if (customId === "shop_prev_page") { - newPage = Math.max(1, sessionState.currentPage - 1); - } else { - const totalPages = Math.ceil(offers.length / ITEMS_PER_PAGE); - newPage = Math.min(totalPages, sessionState.currentPage + 1); - } - - sessionState.currentPage = newPage; - - await interaction.update({ - components: await buildShopPanel( - offers, - sessionState.currentPage, - wallet.coins, - sessionState.selectedOfferId - ), - }); - return; - } -} - -async function handleSelectInteraction( - interaction: StringSelectMenuInteraction, - offers: any[], - currentPage: number, - userId: string, - guildId: string, - shopMessage: Message -): Promise { - // Si implementas un select menu, manejar aquí - await interaction.reply({ - content: "Select menu no implementado aún", - flags: MessageFlags.Ephemeral, - }); -} - -async function buildExpiredPanel(): Promise { - const container: DisplayComponentContainer = { - type: 17, - accent_color: 0x36393f, - components: [ - { - type: 10, - content: "<:timeout:1425685226088169513> **Tienda Expirada**", - }, - { - type: 14, - divider: true, - spacing: 1, - }, - { - type: 10, - content: - "La sesión de tienda ha expirado.\nUsa `!tienda` nuevamente para ver las ofertas.", - }, - ], - }; - - return [container]; -} - -async function buildClosedPanel(): Promise { - const container: DisplayComponentContainer = { - type: 17, - accent_color: 0x36393f, - components: [ - { - type: 10, - content: "<:Sup_res:1420535051162095747> **Tienda Cerrada**", - }, - { - type: 14, - divider: true, - spacing: 1, - }, - { - type: 10, - content: - "¡Gracias por visitar la tienda!\nVuelve pronto. <:onlineshopping:1425684275008897064>", - }, - ], - }; - - return [container]; -} diff --git a/src/.backup/game/toolbreaks.ts b/src/.backup/game/toolbreaks.ts deleted file mode 100644 index f0d99d8..0000000 --- a/src/.backup/game/toolbreaks.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { getToolBreaks } from "../../../game/lib/toolBreakLog"; -import { - buildDisplay, - dividerBlock, - textBlock, -} from "../../../core/lib/componentsV2"; - -export const command: CommandMessage = { - name: "tool-breaks", - type: "message", - aliases: ["rupturas", "breaks"], - cooldown: 4, - description: - "Muestra las últimas rupturas de herramientas registradas (memoria).", - usage: "tool-breaks [limite=10]", - run: async (message, args, _client: Amayo) => { - const guildId = message.guild!.id; - const limit = Math.min(50, Math.max(1, parseInt(args[0] || "10"))); - const events = getToolBreaks(limit, guildId); - - if (!events.length) { - await message.reply( - "No se han registrado rupturas de herramientas todavía." - ); - return; - } - - const blocks = [ - textBlock(`# 🧩 Rupturas de Herramienta (${events.length})`), - dividerBlock(), - ]; - - for (const ev of events) { - const when = new Date(ev.ts).toLocaleTimeString("es-ES", { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); - blocks.push( - textBlock(`• ${when} • \ -Tool: \ -\`${ev.toolKey}\` • ${ - ev.brokenInstance ? "Instancia rota" : "Agotada totalmente" - } • Restantes: ${ev.instancesRemaining} • User: <@${ev.userId}>`) - ); - blocks.push(dividerBlock({ divider: false, spacing: 1 })); - } - - const display = buildDisplay(0x444444, blocks); - await message.reply({ content: "", components: [display] }); - }, -}; diff --git a/src/.backup/game/toolinfo.ts b/src/.backup/game/toolinfo.ts deleted file mode 100644 index 0204ed6..0000000 --- a/src/.backup/game/toolinfo.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { CommandMessage } from "../../../core/types/commands"; -import type Amayo from "../../../core/client"; -import { getInventoryEntry } from "../../../game/economy/service"; -import { - buildDisplay, - textBlock, - dividerBlock, -} from "../../../core/lib/componentsV2"; -import { formatItemLabel, sendDisplayReply } from "./_helpers"; - -// Inspecciona la durabilidad de una herramienta (no apilable) mostrando barra. - -function parseJSON(v: unknown): T | null { - if (!v || typeof v !== "object") return null; - return v as T; -} - -export const command: CommandMessage = { - name: "tool-info", - type: "message", - aliases: ["toolinfo", "herramienta", "inspectar", "inspeccionar"], - cooldown: 3, - description: "Muestra la durabilidad restante de una herramienta por su key.", - usage: "tool-info ", - run: async (message, args, _client: Amayo) => { - const userId = message.author.id; - const guildId = message.guild!.id; - const key = args[0]; - if (!key) { - await message.reply( - "⚠️ Debes indicar la key del item. Ej: `tool-info tool.pickaxe.basic`" - ); - return; - } - try { - const { item, entry } = await getInventoryEntry(userId, guildId, key); - if (!entry || !item) { - await message.reply("❌ No tienes este ítem en tu inventario."); - return; - } - const props = parseJSON(item.props) ?? {}; - const breakable = props.breakable; - if (!breakable || breakable.enabled === false) { - await message.reply("ℹ️ Este ítem no tiene durabilidad activa."); - return; - } - if (item.stackable) { - await message.reply(`ℹ️ Ítem apilable. Cantidad: ${entry.quantity}`); - return; - } - const state = parseJSON(entry.state) ?? {}; - const instances: any[] = Array.isArray(state.instances) - ? state.instances - : []; - const max = Math.max(1, breakable.maxDurability ?? 1); - const label = formatItemLabel( - { key: item.key, name: item.name, icon: item.icon }, - { fallbackIcon: "🛠️" } - ); - const renderBar = (cur: number) => { - const ratio = cur / max; - const totalSegs = 20; - const filled = Math.round(ratio * totalSegs); - return Array.from({ length: totalSegs }) - .map((_, i) => (i < filled ? "█" : "░")) - .join(""); - }; - const durLines = instances.length - ? instances - .map((inst, idx) => { - const cur = Math.min(Math.max(0, inst?.durability ?? max), max); - return `#${idx + 1} [${renderBar(cur)}] ${cur}/${max}`; - }) - .join("\n") - : "(sin instancias)"; - const blocks = [ - textBlock("# 🔍 Herramienta"), - dividerBlock(), - textBlock(`**Item:** ${label}`), - textBlock(`Instancias: ${instances.length}`), - textBlock(durLines), - ]; - const accent = 0x95a5a6; - const display = buildDisplay(accent, blocks); - await sendDisplayReply(message, display); - } catch (e: any) { - await message.reply(`❌ No se pudo inspeccionar: ${e?.message ?? e}`); - } - }, -}; diff --git a/src/.backup/game_core/game/achievements/seed.ts b/src/.backup/game_core/game/achievements/seed.ts deleted file mode 100644 index f8b3a5c..0000000 --- a/src/.backup/game_core/game/achievements/seed.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { prisma } from '../../core/database/prisma'; -import logger from '../../core/lib/logger'; - -/** - * Seed de logros base - */ -export async function seedAchievements(guildId: string | null = null) { - const achievements = [ - // Minería - { - key: 'first_mine', - name: '⛏️ Primera Mina', - description: 'Mina por primera vez', - category: 'mining', - requirements: { type: 'mine_count', value: 1 }, - rewards: { coins: 100 }, - hidden: false, - points: 10 - }, - { - key: 'miner_novice', - name: '⛏️ Minero Novato', - description: 'Mina 10 veces', - category: 'mining', - requirements: { type: 'mine_count', value: 10 }, - rewards: { coins: 500 }, - hidden: false, - points: 20 - }, - { - key: 'miner_expert', - name: '⛏️ Minero Experto', - description: 'Mina 50 veces', - category: 'mining', - requirements: { type: 'mine_count', value: 50 }, - rewards: { coins: 2500 }, - hidden: false, - points: 50 - }, - { - key: 'miner_master', - name: '⛏️ Maestro Minero', - description: 'Mina 100 veces', - category: 'mining', - requirements: { type: 'mine_count', value: 100 }, - rewards: { coins: 10000 }, - hidden: false, - points: 100 - }, - - // Pesca - { - key: 'first_fish', - name: '🎣 Primera Pesca', - description: 'Pesca por primera vez', - category: 'fishing', - requirements: { type: 'fish_count', value: 1 }, - rewards: { coins: 100 }, - hidden: false, - points: 10 - }, - { - key: 'fisher_novice', - name: '🎣 Pescador Novato', - description: 'Pesca 10 veces', - category: 'fishing', - requirements: { type: 'fish_count', value: 10 }, - rewards: { coins: 500 }, - hidden: false, - points: 20 - }, - { - key: 'fisher_expert', - name: '🎣 Pescador Experto', - description: 'Pesca 50 veces', - category: 'fishing', - requirements: { type: 'fish_count', value: 50 }, - rewards: { coins: 2500 }, - hidden: false, - points: 50 - }, - - // Combate - { - key: 'first_fight', - name: '⚔️ Primera Pelea', - description: 'Pelea por primera vez', - category: 'combat', - requirements: { type: 'fight_count', value: 1 }, - rewards: { coins: 150 }, - hidden: false, - points: 10 - }, - { - key: 'warrior_novice', - name: '⚔️ Guerrero Novato', - description: 'Pelea 10 veces', - category: 'combat', - requirements: { type: 'fight_count', value: 10 }, - rewards: { coins: 750 }, - hidden: false, - points: 20 - }, - { - key: 'mob_hunter', - name: '👾 Cazador de Monstruos', - description: 'Derrota 50 mobs', - category: 'combat', - requirements: { type: 'mob_defeat_count', value: 50 }, - rewards: { coins: 3000 }, - hidden: false, - points: 50 - }, - { - key: 'mob_slayer', - name: '👾 Asesino de Monstruos', - description: 'Derrota 200 mobs', - category: 'combat', - requirements: { type: 'mob_defeat_count', value: 200 }, - rewards: { coins: 15000 }, - hidden: false, - points: 100 - }, - - // Economía - { - key: 'first_coins', - name: '💰 Primeras Monedas', - description: 'Gana 1,000 monedas en total', - category: 'economy', - requirements: { type: 'coins_earned', value: 1000 }, - rewards: { coins: 200 }, - hidden: false, - points: 10 - }, - { - key: 'wealthy', - name: '💰 Acaudalado', - description: 'Gana 10,000 monedas en total', - category: 'economy', - requirements: { type: 'coins_earned', value: 10000 }, - rewards: { coins: 2000 }, - hidden: false, - points: 30 - }, - { - key: 'millionaire', - name: '💰 Millonario', - description: 'Gana 100,000 monedas en total', - category: 'economy', - requirements: { type: 'coins_earned', value: 100000 }, - rewards: { coins: 25000 }, - hidden: false, - points: 100 - }, - - // Crafteo - { - key: 'first_craft', - name: '🛠️ Primer Crafteo', - description: 'Craftea tu primer item', - category: 'crafting', - requirements: { type: 'craft_count', value: 1 }, - rewards: { coins: 100 }, - hidden: false, - points: 10 - }, - { - key: 'crafter_expert', - name: '🛠️ Artesano Experto', - description: 'Craftea 50 items', - category: 'crafting', - requirements: { type: 'craft_count', value: 50 }, - rewards: { coins: 5000 }, - hidden: false, - points: 50 - }, - { - key: 'master_crafter', - name: '🛠️ Maestro Artesano', - description: 'Craftea 200 items', - category: 'crafting', - requirements: { type: 'craft_count', value: 200 }, - rewards: { coins: 20000 }, - hidden: false, - points: 100 - } - ]; - - let created = 0; - for (const ach of achievements) { - const existing = await prisma.achievement.findUnique({ - where: { guildId_key: { guildId: guildId || '', key: ach.key } } - }); - - if (!existing) { - await prisma.achievement.create({ - data: { ...ach, guildId: guildId || undefined } - }); - created++; - } - } - - console.log(`Seeded ${created} achievements for guild ${guildId || 'global'}`); - return created; -} - -/** - * Ejecutar si es llamado directamente - */ -if (require.main === module) { - seedAchievements(null) - .then(count => { - console.log(`✅ ${count} achievements seeded`); - process.exit(0); - }) - .catch(error => { - console.error('❌ Error seeding achievements:', error); - process.exit(1); - }); -} diff --git a/src/.backup/game_core/game/achievements/service.ts b/src/.backup/game_core/game/achievements/service.ts deleted file mode 100644 index aa395d1..0000000 --- a/src/.backup/game_core/game/achievements/service.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { prisma } from '../../core/database/prisma'; -import { giveRewards, type Reward } from '../rewards/service'; -import { getOrCreatePlayerStats } from '../stats/service'; -import logger from '../../core/lib/logger'; -import { ensureUserAndGuildExist } from '../core/userService'; - -/** - * Verificar y desbloquear logros según un trigger - */ -export async function checkAchievements( - userId: string, - guildId: string, - trigger: string -): Promise { - try { - // Asegurar que User y Guild existan antes de buscar achievements - await ensureUserAndGuildExist(userId, guildId); - - // Obtener todos los logros del servidor que no estén desbloqueados - const achievements = await prisma.achievement.findMany({ - where: { - OR: [{ guildId }, { guildId: null }], - unlocked: { - none: { - userId, - guildId, - unlockedAt: { not: null } - } - } - } - }); - - const newUnlocks: any[] = []; - const stats = await getOrCreatePlayerStats(userId, guildId); - - for (const achievement of achievements) { - const req = achievement.requirements as any; - - // Verificar si el trigger coincide - if (req.type !== trigger) continue; - - // Obtener o crear progreso del logro - let progress = await prisma.playerAchievement.findUnique({ - where: { - userId_guildId_achievementId: { - userId, - guildId, - achievementId: achievement.id - } - } - }); - - if (!progress) { - progress = await prisma.playerAchievement.create({ - data: { - userId, - guildId, - achievementId: achievement.id, - progress: 0 - } - }); - } - - // Ya desbloqueado - if (progress.unlockedAt) continue; - - // Obtener el valor actual según el tipo de requisito - let currentValue = 0; - switch (req.type) { - case 'mine_count': - currentValue = stats.minesCompleted; - break; - case 'fish_count': - currentValue = stats.fishingCompleted; - break; - case 'fight_count': - currentValue = stats.fightsCompleted; - break; - case 'farm_count': - currentValue = stats.farmsCompleted; - break; - case 'mob_defeat_count': - currentValue = stats.mobsDefeated; - break; - case 'craft_count': - currentValue = stats.itemsCrafted; - break; - case 'coins_earned': - currentValue = stats.totalCoinsEarned; - break; - case 'damage_dealt': - currentValue = stats.damageDealt; - break; - default: - continue; - } - - // Actualizar progreso - await prisma.playerAchievement.update({ - where: { id: progress.id }, - data: { progress: currentValue } - }); - - // Verificar si se desbloqueó - if (currentValue >= req.value) { - await prisma.playerAchievement.update({ - where: { id: progress.id }, - data: { - unlockedAt: new Date(), - progress: req.value - } - }); - - // Dar recompensas si las hay - if (achievement.rewards) { - await giveRewards(userId, guildId, achievement.rewards as Reward, `achievement:${achievement.key}`); - } - - newUnlocks.push(achievement); - } - } - - return newUnlocks; - } catch (error) { - console.error(`Error checking achievements for ${userId}:`, error); - return []; - } -} - -/** - * Obtener logros desbloqueados de un jugador - */ -export async function getPlayerAchievements(userId: string, guildId: string) { - const unlocked = await prisma.playerAchievement.findMany({ - where: { - userId, - guildId, - unlockedAt: { not: null } - }, - include: { - achievement: true - }, - orderBy: { - unlockedAt: 'desc' - } - }); - - const inProgress = await prisma.playerAchievement.findMany({ - where: { - userId, - guildId, - unlockedAt: null - }, - include: { - achievement: true - }, - orderBy: { - progress: 'desc' - } - }); - - return { unlocked, inProgress }; -} - -/** - * Obtener progreso de un logro específico - */ -export async function getAchievementProgress( - userId: string, - guildId: string, - achievementKey: string -) { - const achievement = await prisma.achievement.findUnique({ - where: { guildId_key: { guildId, key: achievementKey } } - }); - - if (!achievement) return null; - - const progress = await prisma.playerAchievement.findUnique({ - where: { - userId_guildId_achievementId: { - userId, - guildId, - achievementId: achievement.id - } - } - }); - - if (!progress) return { current: 0, required: (achievement.requirements as any).value, percentage: 0 }; - - const required = (achievement.requirements as any).value; - const percentage = Math.min(100, Math.floor((progress.progress / required) * 100)); - - return { - current: progress.progress, - required, - percentage, - unlocked: !!progress.unlockedAt - }; -} - -/** - * Crear barra de progreso visual - */ -export function createProgressBar(current: number, total: number, width: number = 10): string { - const filled = Math.floor((current / total) * width); - const empty = Math.max(0, width - filled); - const percentage = Math.min(100, Math.floor((current / total) * 100)); - return `${'█'.repeat(filled)}${'░'.repeat(empty)} ${percentage}%`; -} - -/** - * Obtener estadísticas de logros del jugador - */ -export async function getAchievementStats(userId: string, guildId: string) { - const total = await prisma.achievement.count({ - where: { OR: [{ guildId }, { guildId: null }] } - }); - - const unlocked = await prisma.playerAchievement.count({ - where: { - userId, - guildId, - unlockedAt: { not: null } - } - }); - - const totalPoints = await prisma.playerAchievement.findMany({ - where: { - userId, - guildId, - unlockedAt: { not: null } - }, - include: { - achievement: true - } - }).then(achievements => - achievements.reduce((sum, pa) => sum + (pa.achievement.points || 0), 0) - ); - - return { - total, - unlocked, - locked: total - unlocked, - percentage: total > 0 ? Math.floor((unlocked / total) * 100) : 0, - points: totalPoints - }; -} diff --git a/src/.backup/game_core/game/combat/attacksWorker.ts b/src/.backup/game_core/game/combat/attacksWorker.ts deleted file mode 100644 index 8d936a3..0000000 --- a/src/.backup/game_core/game/combat/attacksWorker.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { prisma } from "../../core/database/prisma"; -import { - ensurePlayerState, - getEquipment, - getEffectiveStats, - adjustHP, -} from "./equipmentService"; -import { reduceToolDurability } from "../minigames/service"; - -function getNumber(v: any, fallback = 0) { - return typeof v === "number" ? v : fallback; -} - -export async function processScheduledAttacks(limit = 25) { - const now = new Date(); - const jobs = await prisma.scheduledMobAttack.findMany({ - where: { status: "scheduled", scheduleAt: { lte: now } }, - orderBy: { scheduleAt: "asc" }, - take: limit, - }); - for (const job of jobs) { - try { - await prisma.$transaction(async (tx) => { - // marcar processing - await tx.scheduledMobAttack.update({ - where: { id: job.id }, - data: { status: "processing" }, - }); - - const mob = await tx.mob.findUnique({ where: { id: job.mobId } }); - if (!mob) throw new Error("Mob inexistente"); - const stats = (mob.stats as any) || {}; - const mobAttack = Math.max(0, getNumber(stats.attack, 5)); - - await ensurePlayerState(job.userId, job.guildId); - const eff = await getEffectiveStats(job.userId, job.guildId); - const dmg = Math.max(1, mobAttack - eff.defense); - - // aplicar daño - await adjustHP(job.userId, job.guildId, -dmg); - - // desgastar arma equipada si existe - const { eq, weapon } = await getEquipment(job.userId, job.guildId); - if (weapon) { - // buscar por key para reducir durabilidad con multiplicador de combate (50%) - // weapon tiene id; buscamos para traer key - const full = await tx.economyItem.findUnique({ - where: { id: weapon.id }, - }); - if (full) { - await reduceToolDurability( - job.userId, - job.guildId, - full.key, - "combat" - ); - } - } - - // finalizar - await tx.scheduledMobAttack.update({ - where: { id: job.id }, - data: { status: "done", processedAt: new Date() }, - }); - }); - } catch (e) { - await prisma.scheduledMobAttack.update({ - where: { id: job.id }, - data: { - status: "failed", - processedAt: new Date(), - metadata: { error: String(e) } as any, - }, - }); - } - } - return { processed: jobs.length } as const; -} - -if (require.main === module) { - processScheduledAttacks() - .then((r) => { - console.log("[attacksWorker] processed", r.processed); - process.exit(0); - }) - .catch((e) => { - console.error(e); - process.exit(1); - }); -} diff --git a/src/.backup/game_core/game/combat/equipmentService.ts b/src/.backup/game_core/game/combat/equipmentService.ts deleted file mode 100644 index 4c6e7e8..0000000 --- a/src/.backup/game_core/game/combat/equipmentService.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { prisma } from "../../core/database/prisma"; -import { - getActiveStatusEffects, - computeDerivedModifiers, -} from "./statusEffectsService"; -import type { ItemProps } from "../economy/types"; -import { ensureUserAndGuildExist } from "../core/userService"; -import { parseItemProps } from "../core/utils"; - -export async function ensurePlayerState(userId: string, guildId: string) { - // Asegurar que User y Guild existan antes de crear/buscar state - await ensureUserAndGuildExist(userId, guildId); - - return prisma.playerState.upsert({ - where: { userId_guildId: { userId, guildId } }, - update: {}, - create: { userId, guildId }, - }); -} - -export async function getEquipment(userId: string, guildId: string) { - // Asegurar que User y Guild existan antes de crear/buscar equipment - await ensureUserAndGuildExist(userId, guildId); - - const eq = await prisma.playerEquipment.upsert({ - where: { userId_guildId: { userId, guildId } }, - update: {}, - create: { userId, guildId }, - }); - const weapon = eq.weaponItemId - ? await prisma.economyItem.findUnique({ where: { id: eq.weaponItemId } }) - : null; - const armor = eq.armorItemId - ? await prisma.economyItem.findUnique({ where: { id: eq.armorItemId } }) - : null; - const cape = eq.capeItemId - ? await prisma.economyItem.findUnique({ where: { id: eq.capeItemId } }) - : null; - return { eq, weapon, armor, cape } as const; -} - -export async function setEquipmentSlot( - userId: string, - guildId: string, - slot: "weapon" | "armor" | "cape", - itemId: string | null -) { - // Asegurar que User y Guild existan antes de crear/actualizar equipment - await ensureUserAndGuildExist(userId, guildId); - - const data = - slot === "weapon" - ? { weaponItemId: itemId } - : slot === "armor" - ? { armorItemId: itemId } - : { capeItemId: itemId }; - return prisma.playerEquipment.upsert({ - where: { userId_guildId: { userId, guildId } }, - update: data, - create: { userId, guildId, ...data }, - }); -} - -export type EffectiveStats = { - damage: number; // daño efectivo (con racha + efectos) - defense: number; // defensa efectiva (con efectos) - maxHp: number; - hp: number; - baseDamage?: number; // daño base antes de status effects - baseDefense?: number; // defensa base antes de status effects -}; - -async function getMutationBonuses( - userId: string, - guildId: string, - itemId?: string | null -) { - if (!itemId) return { damageBonus: 0, defenseBonus: 0, maxHpBonus: 0 }; - const inv = await prisma.inventoryEntry.findUnique({ - where: { userId_guildId_itemId: { userId, guildId, itemId } }, - }); - if (!inv) return { damageBonus: 0, defenseBonus: 0, maxHpBonus: 0 }; - const links = await prisma.inventoryItemMutation.findMany({ - where: { inventoryId: inv.id }, - include: { mutation: true }, - }); - let damageBonus = 0, - defenseBonus = 0, - maxHpBonus = 0; - for (const l of links) { - const eff = (l.mutation.effects as any) || {}; - if (typeof eff.damageBonus === "number") damageBonus += eff.damageBonus; - if (typeof eff.defenseBonus === "number") defenseBonus += eff.defenseBonus; - if (typeof eff.maxHpBonus === "number") maxHpBonus += eff.maxHpBonus; - } - return { damageBonus, defenseBonus, maxHpBonus }; -} - -export async function getEffectiveStats( - userId: string, - guildId: string -): Promise { - const state = await ensurePlayerState(userId, guildId); - const { weapon, armor, cape } = await getEquipment(userId, guildId); - const w = parseItemProps(weapon?.props); - const a = parseItemProps(armor?.props); - const c = parseItemProps(cape?.props); - - const mutW = await getMutationBonuses(userId, guildId, weapon?.id ?? null); - const mutA = await getMutationBonuses(userId, guildId, armor?.id ?? null); - const mutC = await getMutationBonuses(userId, guildId, cape?.id ?? null); - - let damage = Math.max(0, (w.damage ?? 0) + mutW.damageBonus); - const defenseBase = Math.max(0, (a.defense ?? 0) + mutA.defenseBonus); - const maxHp = Math.max( - 1, - state.maxHp + (c.maxHpBonus ?? 0) + mutC.maxHpBonus - ); - const hp = Math.min(state.hp, maxHp); - // Buff por racha de victorias: 1% daño extra cada 3 victorias consecutivas (cap 30%) - try { - const stats = await prisma.playerStats.findUnique({ - where: { userId_guildId: { userId, guildId } }, - }); - if (stats) { - const streak = stats.currentWinStreak || 0; - const steps = Math.floor(streak / 3); - const bonusPct = Math.min(steps * 0.01, 0.3); // cap 30% - if (bonusPct > 0) - damage = Math.max(0, Math.round(damage * (1 + bonusPct))); - } - } catch { - // silencioso: si falla stats no bloquea - } - // Aplicar efectos de estado activos (FATIGUE etc.) - try { - const effects = await getActiveStatusEffects(userId, guildId); - if (effects.length) { - const { damageMultiplier, defenseMultiplier } = computeDerivedModifiers( - effects.map((e) => ({ type: e.type, magnitude: e.magnitude })) - ); - const baseDamage = damage; - const baseDefense = defenseBase; - damage = Math.max(0, Math.round(damage * damageMultiplier)); - const adjustedDefense = Math.max( - 0, - Math.round(defenseBase * defenseMultiplier) - ); - return { - damage, - defense: adjustedDefense, - maxHp, - hp, - baseDamage, - baseDefense, - }; - } - } catch { - // silencioso - } - return { - damage, - defense: defenseBase, - maxHp, - hp, - baseDamage: damage, - baseDefense: defenseBase, - }; -} - -export async function adjustHP(userId: string, guildId: string, delta: number) { - const state = await ensurePlayerState(userId, guildId); - const { cape } = await getEquipment(userId, guildId); - const c = parseItemProps(cape?.props); - const maxHp = Math.max(1, state.maxHp + (c.maxHpBonus ?? 0)); - const next = Math.min(maxHp, Math.max(0, state.hp + delta)); - return prisma.playerState.update({ - where: { userId_guildId: { userId, guildId } }, - data: { hp: next, maxHp }, - }); -} diff --git a/src/.backup/game_core/game/combat/statusEffectsService.ts b/src/.backup/game_core/game/combat/statusEffectsService.ts deleted file mode 100644 index 1da847f..0000000 --- a/src/.backup/game_core/game/combat/statusEffectsService.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { prisma } from "../../core/database/prisma"; - -export type StatusEffectType = "FATIGUE" | string; - -export interface StatusEffectOptions { - magnitude?: number; // porcentaje o valor genérico según tipo - durationMs?: number; // duración; si no se pasa => permanente - data?: Record; -} - -export async function applyStatusEffect( - userId: string, - guildId: string, - type: StatusEffectType, - opts?: StatusEffectOptions -) { - const now = Date.now(); - const expiresAt = opts?.durationMs ? new Date(now + opts.durationMs) : null; - return prisma.playerStatusEffect.upsert({ - where: { userId_guildId_type: { userId, guildId, type } }, - update: { - magnitude: opts?.magnitude ?? 0, - expiresAt, - data: opts?.data ?? {}, - }, - create: { - userId, - guildId, - type, - magnitude: opts?.magnitude ?? 0, - expiresAt, - data: opts?.data ?? {}, - }, - }); -} - -export async function getActiveStatusEffects(userId: string, guildId: string) { - // Limpieza perezosa de expirados - await prisma.playerStatusEffect.deleteMany({ - where: { userId, guildId, expiresAt: { lt: new Date() } }, - }); - return prisma.playerStatusEffect.findMany({ - where: { userId, guildId }, - }); -} - -export function computeDerivedModifiers( - effects: { type: string; magnitude: number }[] -) { - let damageMultiplier = 1; - let defenseMultiplier = 1; - for (const e of effects) { - switch (e.type) { - case "FATIGUE": - // Reducción lineal: magnitude = 0.15 => -15% daño y -10% defensa, configurable - damageMultiplier *= 1 - Math.min(0.9, e.magnitude); // cap 90% reducción - defenseMultiplier *= 1 - Math.min(0.9, e.magnitude * 0.66); - break; - default: - break; // otros efectos futuros - } - } - return { damageMultiplier, defenseMultiplier }; -} - -export async function applyDeathFatigue( - userId: string, - guildId: string, - magnitude = 0.15, - minutes = 5 -) { - return applyStatusEffect(userId, guildId, "FATIGUE", { - magnitude, - durationMs: minutes * 60 * 1000, - data: { reason: "death" }, - }); -} - -export async function removeStatusEffect( - userId: string, - guildId: string, - type: StatusEffectType -) { - await prisma.playerStatusEffect.deleteMany({ - where: { userId, guildId, type }, - }); -} - -export async function clearAllStatusEffects(userId: string, guildId: string) { - await prisma.playerStatusEffect.deleteMany({ where: { userId, guildId } }); -} diff --git a/src/.backup/game_core/game/consumables/service.ts b/src/.backup/game_core/game/consumables/service.ts deleted file mode 100644 index 2140910..0000000 --- a/src/.backup/game_core/game/consumables/service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { prisma } from "../../core/database/prisma"; -import { assertNotOnCooldown, setCooldown } from "../cooldowns/service"; -import { findItemByKey, consumeItemByKey } from "../economy/service"; -import type { ItemProps } from "../economy/types"; -import { getEffectiveStats, adjustHP } from "../combat/equipmentService"; -import { parseItemProps } from "../core/utils"; -import { getCooldownKeyForFood, calculateHealingFromFood } from "./utils"; - -export async function useConsumableByKey( - userId: string, - guildId: string, - itemKey: string -) { - const item = await findItemByKey(guildId, itemKey); - if (!item) throw new Error("Ítem no encontrado"); - const props = parseItemProps(item.props); - const food = props.food; - if (!food) throw new Error("Este ítem no es consumible"); - - const cdKey = getCooldownKeyForFood(item.key, food); - await assertNotOnCooldown(userId, guildId, cdKey); - - // Calcular sanación - const stats = await getEffectiveStats(userId, guildId); - const heal = calculateHealingFromFood(food, stats.maxHp); - - // Consumir el ítem - const { consumed } = await consumeItemByKey(userId, guildId, item.key, 1); - if (consumed <= 0) throw new Error("No tienes este ítem"); - - // Aplicar curación - await adjustHP(userId, guildId, heal); - - // Setear cooldown si corresponde - if (food.cooldownSeconds && food.cooldownSeconds > 0) { - await setCooldown(userId, guildId, cdKey, food.cooldownSeconds); - } - - return { healed: heal } as const; -} diff --git a/src/.backup/game_core/game/consumables/utils.ts b/src/.backup/game_core/game/consumables/utils.ts deleted file mode 100644 index fccc013..0000000 --- a/src/.backup/game_core/game/consumables/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -export function getCooldownKeyForFood( - itemKey: string, - foodProps: any | undefined -) { - return foodProps?.cooldownKey ?? `food:${itemKey}`; -} - -export function calculateHealingFromFood( - foodProps: any | undefined, - maxHp: number -) { - const flat = Math.max(0, (foodProps?.healHp ?? 0) as number); - const perc = Math.max(0, (foodProps?.healPercent ?? 0) as number); - const byPerc = Math.floor((perc / 100) * maxHp); - return Math.max(1, flat + byPerc); -} diff --git a/src/.backup/game_core/game/cooldowns/service.ts b/src/.backup/game_core/game/cooldowns/service.ts deleted file mode 100644 index 0a63e16..0000000 --- a/src/.backup/game_core/game/cooldowns/service.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { prisma } from '../../core/database/prisma'; -import { ensureUserAndGuildExist } from '../core/userService'; - -export async function getCooldown(userId: string, guildId: string, key: string) { - return prisma.actionCooldown.findUnique({ where: { userId_guildId_key: { userId, guildId, key } } }); -} - -export async function setCooldown(userId: string, guildId: string, key: string, seconds: number) { - // Asegurar que User y Guild existan antes de crear/actualizar cooldown - await ensureUserAndGuildExist(userId, guildId); - - const until = new Date(Date.now() + Math.max(0, seconds) * 1000); - return prisma.actionCooldown.upsert({ - where: { userId_guildId_key: { userId, guildId, key } }, - update: { until }, - create: { userId, guildId, key, until }, - }); -} - -export async function assertNotOnCooldown(userId: string, guildId: string, key: string) { - const cd = await getCooldown(userId, guildId, key); - if (cd && cd.until > new Date()) throw new Error('Cooldown activo'); -} - diff --git a/src/.backup/game_core/game/core/userService.ts b/src/.backup/game_core/game/core/userService.ts deleted file mode 100644 index d20f9ac..0000000 --- a/src/.backup/game_core/game/core/userService.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { prisma } from '../../core/database/prisma'; -import logger from '../../core/lib/logger'; - -/** - * Asegura que existan los registros de User y Guild en la base de datos. - * - * **PROBLEMA RESUELTO**: Cuando un usuario nuevo usa comandos de juego (como !inventario, !craftear, etc.), - * las funciones como getOrCreateWallet(), getOrCreatePlayerStats(), etc. intentaban crear registros - * con foreign keys a User y Guild que no existían, causando errores de constraint. - * - * **SOLUCIÓN**: Esta función garantiza que User y Guild existan ANTES de crear cualquier dato relacionado. - * - * @param userId - Discord User ID - * @param guildId - Discord Guild ID - * @param guildName - Nombre del servidor (opcional, para crear Guild si no existe) - * @returns Promise - */ -export async function ensureUserAndGuildExist( - userId: string, - guildId: string, - guildName?: string -): Promise { - try { - // Verificar y crear User si no existe - await prisma.user.upsert({ - where: { id: userId }, - update: {}, // No actualizamos nada si ya existe - create: { id: userId } - }); - - // Verificar y crear Guild si no existe - await prisma.guild.upsert({ - where: { id: guildId }, - update: {}, // No actualizamos nada si ya existe - create: { - id: guildId, - name: guildName || 'Unknown Server', - prefix: '!' - } - }); - } catch (error) { - logger.error({ error }, 'Error ensuring User and Guild exist'); - throw error; - } -} - -/** - * Asegura que un User exista en la base de datos. - * Útil cuando solo necesitas garantizar que el usuario existe. - * - * @param userId - Discord User ID - * @returns Promise - */ -export async function ensureUserExists(userId: string): Promise { - try { - await prisma.user.upsert({ - where: { id: userId }, - update: {}, - create: { id: userId } - }); - } catch (error) { - logger.error({ userId, error }, 'Error ensuring User exists'); - throw error; - } -} - -/** - * Asegura que un Guild exista en la base de datos. - * - * @param guildId - Discord Guild ID - * @param guildName - Nombre del servidor (opcional) - * @returns Promise - */ -export async function ensureGuildExists(guildId: string, guildName?: string): Promise { - try { - await prisma.guild.upsert({ - where: { id: guildId }, - update: {}, - create: { - id: guildId, - name: guildName || 'Unknown Server', - prefix: '!' - } - }); - } catch (error) { - logger.error({ guildId, error }, 'Error ensuring Guild exists'); - throw error; - } -} diff --git a/src/.backup/game_core/game/core/utils.ts b/src/.backup/game_core/game/core/utils.ts deleted file mode 100644 index a16bdfc..0000000 --- a/src/.backup/game_core/game/core/utils.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { prisma } from "../../core/database/prisma"; -import type { Prisma } from "@prisma/client"; - -export function now(): Date { - return new Date(); -} - -export function isWithin( - date: Date, - from?: Date | null, - to?: Date | null -): boolean { - if (from && date < from) return false; - if (to && date > to) return false; - return true; -} - -export function ensureArray(v: T[] | undefined | null): T[] { - return Array.isArray(v) ? v : []; -} - -export function parseItemProps(json: unknown): any { - if (!json || typeof json !== "object") return {}; - return json as any; -} - -export function parseState(json: unknown): any { - if (!json || typeof json !== "object") return {}; - return json as any; -} - -export async function updateInventoryEntryState( - userId: string, - guildId: string, - itemId: string, - state: any -) { - const quantity = - state.instances && Array.isArray(state.instances) - ? state.instances.length - : 0; - return prisma.inventoryEntry.update({ - where: { userId_guildId_itemId: { userId, guildId, itemId } }, - data: { state: state as unknown as Prisma.InputJsonValue, quantity }, - }); -} - -export default { - now, - isWithin, - ensureArray, - parseItemProps, - parseState, - updateInventoryEntryState, -}; diff --git a/src/.backup/game_core/game/economy/seedPurgePotion.ts b/src/.backup/game_core/game/economy/seedPurgePotion.ts deleted file mode 100644 index 4d2f24a..0000000 --- a/src/.backup/game_core/game/economy/seedPurgePotion.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { prisma } from "../../core/database/prisma"; - -// Seed para crear el ítem de purga de efectos (potion.purga) -// Ejecutar manualmente una vez. -// node -r ts-node/register src/game/economy/seedPurgePotion.ts (según tu setup) - -async function main() { - const key = "potion.purga"; - const existing = await prisma.economyItem.findFirst({ - where: { key, guildId: null }, - }); - if (existing) { - console.log("Ya existe potion.purga (global)"); - return; - } - const item = await prisma.economyItem.create({ - data: { - key, - name: "Poción de Purga", - description: - "Elimina todos tus efectos de estado activos al usar el comando !efectos purgar.", - category: "consumable", - icon: "🧪", - stackable: true, - props: { usable: true, purgeAllEffects: true }, - tags: ["purge", "status", "utility"], - }, - }); - console.log("Creado:", item.id, item.key); -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/src/.backup/game_core/game/economy/service.ts b/src/.backup/game_core/game/economy/service.ts deleted file mode 100644 index 2869d99..0000000 --- a/src/.backup/game_core/game/economy/service.ts +++ /dev/null @@ -1,494 +0,0 @@ -import { prisma } from "../../core/database/prisma"; -import type { - ItemProps, - InventoryState, - Price, - OpenChestResult, -} from "./types"; -import type { Prisma } from "@prisma/client"; -import { ensureUserAndGuildExist } from "../core/userService"; -import coreUtils from "../core/utils"; - -// Reusar utilidades centrales desde game core -const { - now, - isWithin, - ensureArray, - parseItemProps, - parseState, - updateInventoryEntryState, -} = coreUtils as any; - -// Resuelve un EconomyItem por key con alcance de guild o global -export async function findItemByKey(guildId: string, key: string) { - // buscamos ítem por guildId específico primero, si no, por global (guildId null) - const item = await prisma.economyItem.findFirst({ - where: { - key, - OR: [{ guildId }, { guildId: null }], - }, - orderBy: [ - // preferir coincidencia del servidor - { guildId: "desc" }, - ], - }); - return item; -} - -export async function getOrCreateWallet(userId: string, guildId: string) { - // Asegurar que User y Guild existan antes de crear/buscar wallet - await ensureUserAndGuildExist(userId, guildId); - - return prisma.economyWallet.upsert({ - where: { userId_guildId: { userId, guildId } }, - update: {}, - create: { userId, guildId, coins: 25 }, - }); -} - -export async function adjustCoins( - userId: string, - guildId: string, - delta: number -) { - const wallet = await getOrCreateWallet(userId, guildId); - const next = Math.max(0, wallet.coins + delta); - return prisma.economyWallet.update({ - where: { userId_guildId: { userId, guildId } }, - data: { coins: next }, - }); -} - -export type EnsureInventoryOptions = { createIfMissing?: boolean }; - -export async function getInventoryEntryByItemId( - userId: string, - guildId: string, - itemId: string, - opts?: EnsureInventoryOptions -) { - const existing = await prisma.inventoryEntry.findUnique({ - where: { userId_guildId_itemId: { userId, guildId, itemId } }, - }); - if (existing) return existing; - if (!opts?.createIfMissing) return null; - // Asegurar que User y Guild existan antes de crear inventoryEntry para evitar - // errores de constraint por foreign keys inexistentes. - await ensureUserAndGuildExist(userId, guildId); - return prisma.inventoryEntry.create({ - data: { userId, guildId, itemId, quantity: 0 }, - }); -} - -export async function getInventoryEntry( - userId: string, - guildId: string, - itemKey: string, - opts?: EnsureInventoryOptions -) { - const item = await findItemByKey(guildId, itemKey); - if (!item) throw new Error(`Item key not found: ${itemKey}`); - const entry = await getInventoryEntryByItemId(userId, guildId, item.id, opts); - return { item, entry } as const; -} - -// utilidades parseItemProps, parseState, ensureArray y updateInventoryEntryState -// provistas por coreUtils importado arriba - -function checkUsableWindow(item: { - usableFrom: Date | null; - usableTo: Date | null; - props: any; -}) { - const props = parseItemProps(item.props); - const from = props.usableFrom ? new Date(props.usableFrom) : item.usableFrom; - const to = props.usableTo ? new Date(props.usableTo) : item.usableTo; - if (!isWithin(now(), from ?? null, to ?? null)) { - throw new Error("Item no usable por ventana de tiempo"); - } -} - -function checkAvailableWindow(item: { - availableFrom: Date | null; - availableTo: Date | null; - props: any; -}) { - const props = parseItemProps(item.props); - const from = props.availableFrom - ? new Date(props.availableFrom) - : item.availableFrom; - const to = props.availableTo ? new Date(props.availableTo) : item.availableTo; - if (!isWithin(now(), from ?? null, to ?? null)) { - throw new Error("Item no disponible para adquirir"); - } -} - -// Agrega cantidad respetando maxPerInventory y stackable -export async function addItemByKey( - userId: string, - guildId: string, - itemKey: string, - qty: number -) { - if (qty <= 0) return { added: 0 } as const; - const found = await getInventoryEntry(userId, guildId, itemKey, { - createIfMissing: true, - }); - const item = found.item; - const entry = found.entry; - if (!entry) throw new Error("No se pudo crear/obtener inventario"); - checkAvailableWindow(item); - - const max = item.maxPerInventory ?? Number.MAX_SAFE_INTEGER; - if (item.stackable) { - const currentQty = entry.quantity ?? 0; - const added = Math.max(0, Math.min(qty, Math.max(0, max - currentQty))); - if (added === 0) return { added: 0 } as const; - const updated = await prisma.inventoryEntry.update({ - where: { userId_guildId_itemId: { userId, guildId, itemId: item.id } }, - data: { quantity: { increment: added } }, - }); - return { added, entry: updated } as const; - } else { - // No apilable: usar state.instances - const state = parseState(entry.state); - state.instances = ensureArray(state.instances); - const canAdd = Math.max( - 0, - Math.min(qty, Math.max(0, max - state.instances.length)) - ); - // Inicializar durabilidad si corresponde - const props = parseItemProps(item.props); - const breakable = props.breakable; - const maxDurability = - breakable?.enabled !== false ? breakable?.maxDurability : undefined; - for (let i = 0; i < canAdd; i++) { - if (maxDurability && maxDurability > 0) { - state.instances.push({ durability: maxDurability }); - } else { - state.instances.push({}); - } - } - const updated = await updateInventoryEntryState( - userId, - guildId, - item.id, - state - ); - return { added: canAdd, entry: updated } as const; - } -} - -export async function consumeItemByKey( - userId: string, - guildId: string, - itemKey: string, - qty: number -) { - if (qty <= 0) return { consumed: 0 } as const; - const { item, entry } = await getInventoryEntry(userId, guildId, itemKey); - if (!entry || (entry.quantity ?? 0) <= 0) return { consumed: 0 } as const; - - if (item.stackable) { - const consumed = Math.min(qty, entry.quantity); - const updated = await prisma.inventoryEntry.update({ - where: { userId_guildId_itemId: { userId, guildId, itemId: item.id } }, - data: { quantity: { decrement: consumed } }, - }); - return { consumed, entry: updated } as const; - } else { - const state = parseState(entry.state); - state.instances = ensureArray(state.instances); - const consumed = Math.min(qty, state.instances.length); - if (consumed === 0) return { consumed: 0 } as const; - state.instances.splice(0, consumed); - const updated = await updateInventoryEntryState( - userId, - guildId, - item.id, - state - ); - return { consumed, entry: updated } as const; - } -} - -export async function openChestByKey( - userId: string, - guildId: string, - itemKey: string -): Promise { - const { item, entry } = await getInventoryEntry(userId, guildId, itemKey); - if (!entry || (entry.quantity ?? 0) <= 0) - throw new Error("No tienes este cofre"); - checkUsableWindow(item); - - const props = parseItemProps(item.props); - const chest = props.chest ?? {}; - if (!chest.enabled) throw new Error("Este ítem no se puede abrir"); - const rewards: any[] = Array.isArray(chest.rewards) ? chest.rewards : []; - const mode = chest.randomMode || "all"; - const result: OpenChestResult = { - coinsDelta: 0, - itemsToAdd: [], - rolesToGrant: [], - consumed: false, - }; - - function pickOneWeighted( - arr: T[] - ): T | null { - const prepared = arr.map((a) => ({ - ...a, - _w: a.probability != null ? Math.max(0, a.probability) : 1, - })); - const total = prepared.reduce((s, a) => s + a._w, 0); - if (total <= 0) return null; - let r = Math.random() * total; - for (const a of prepared) { - r -= a._w; - if (r <= 0) return a; - } - return prepared[prepared.length - 1] ?? null; - } - - if (mode === "single") { - const one = pickOneWeighted(rewards); - if (one) { - if (one.type === "coins") result.coinsDelta += Math.max(0, one.amount); - else if (one.type === "item") - result.itemsToAdd.push({ - itemKey: one.itemKey, - itemId: one.itemId, - qty: one.qty, - }); - else if (one.type === "role") result.rolesToGrant.push(one.roleId); - } - } else { - // 'all' y 'roll-each': procesar cada reward con probabilidad (default 100%) - for (const r of rewards) { - const p = r.probability != null ? Math.max(0, r.probability) : 1; // p en [0,1] recomendado; si usan valores >1 se interpretan como peso - // Si p > 1 asumimos error o peso -> para modo 'all' lo tratamos como 1 (100%) - const chance = p > 1 ? 1 : p; // normalizado - if (Math.random() <= chance) { - if (r.type === "coins") result.coinsDelta += Math.max(0, r.amount); - else if (r.type === "item") - result.itemsToAdd.push({ - itemKey: r.itemKey, - itemId: r.itemId, - qty: r.qty, - }); - else if (r.type === "role") result.rolesToGrant.push(r.roleId); - } - } - } - - // Roles fijos adicionales en chest.roles - if (Array.isArray(chest.roles) && chest.roles.length) { - for (const roleId of chest.roles) { - if (typeof roleId === "string" && roleId.length > 0) - result.rolesToGrant.push(roleId); - } - } - - if (result.coinsDelta) await adjustCoins(userId, guildId, result.coinsDelta); - for (const it of result.itemsToAdd) { - if (it.itemKey) await addItemByKey(userId, guildId, it.itemKey, it.qty); - else if (it.itemId) { - const item = await prisma.economyItem.findUnique({ - where: { id: it.itemId }, - }); - if (item) await addItemByKey(userId, guildId, item.key, it.qty); - } - } - - if (chest.consumeOnOpen) { - const c = await consumeItemByKey(userId, guildId, itemKey, 1); - result.consumed = c.consumed > 0; - } - - return result; -} - -export async function craftByProductKey( - userId: string, - guildId: string, - productKey: string -) { - const product = await findItemByKey(guildId, productKey); - if (!product) throw new Error(`Producto no encontrado: ${productKey}`); - const recipe = await prisma.itemRecipe.findUnique({ - where: { productItemId: product.id }, - include: { ingredients: true }, - }); - if (!recipe) throw new Error("No existe receta para este ítem"); - - // Verificar ingredientes suficientes - const shortages: string[] = []; - for (const ing of recipe.ingredients) { - const inv = await prisma.inventoryEntry.findUnique({ - where: { userId_guildId_itemId: { userId, guildId, itemId: ing.itemId } }, - }); - const have = inv?.quantity ?? 0; - if (have < ing.quantity) shortages.push(ing.itemId); - } - if (shortages.length) throw new Error("Ingredientes insuficientes"); - - // Consumir ingredientes - for (const ing of recipe.ingredients) { - await prisma.inventoryEntry.update({ - where: { userId_guildId_itemId: { userId, guildId, itemId: ing.itemId } }, - data: { quantity: { decrement: ing.quantity } }, - }); - } - - // Agregar producto - const add = await addItemByKey( - userId, - guildId, - product.key, - recipe.productQuantity - ); - return { added: add.added, product } as const; -} - -export async function buyFromOffer( - userId: string, - guildId: string, - offerId: string, - qty = 1 -) { - if (qty <= 0) throw new Error("Cantidad inválida"); - const offer = await prisma.shopOffer.findUnique({ where: { id: offerId } }); - if (!offer || offer.guildId !== guildId) - throw new Error("Oferta no encontrada"); - if (!offer.enabled) throw new Error("Oferta deshabilitada"); - const nowD = now(); - if (!isWithin(nowD, offer.startAt ?? null, offer.endAt ?? null)) - throw new Error("Oferta fuera de fecha"); - - const price = (offer.price as unknown as Price) ?? {}; - // Limites - if (offer.perUserLimit != null) { - const count = await prisma.shopPurchase.aggregate({ - where: { offerId: offer.id, userId, guildId }, - _sum: { qty: true }, - }); - const already = count._sum.qty ?? 0; - if (already + qty > offer.perUserLimit) - throw new Error("Excede el límite por usuario"); - } - - if (offer.stock != null) { - if (offer.stock < qty) throw new Error("Stock insuficiente"); - } - - // Cobro: coins - if (price.coins && price.coins > 0) { - const wallet = await getOrCreateWallet(userId, guildId); - const total = price.coins * qty; - if (wallet.coins < total) throw new Error("Monedas insuficientes"); - await prisma.economyWallet.update({ - where: { userId_guildId: { userId, guildId } }, - data: { coins: wallet.coins - total }, - }); - } - // Cobro: items - if (price.items && price.items.length) { - for (const comp of price.items) { - const key = comp.itemKey; - const compQty = comp.qty * qty; - let itemId: string | null = null; - if (key) { - const it = await findItemByKey(guildId, key); - if (!it) throw new Error(`Item de precio no encontrado: ${key}`); - itemId = it.id; - } else if (comp.itemId) { - itemId = comp.itemId; - } - if (!itemId) throw new Error("Item de precio inválido"); - const inv = await prisma.inventoryEntry.findUnique({ - where: { userId_guildId_itemId: { userId, guildId, itemId } }, - }); - if ((inv?.quantity ?? 0) < compQty) - throw new Error("No tienes suficientes items para pagar"); - } - // si todo está ok, descontar - for (const comp of price.items) { - const key = comp.itemKey; - const compQty = comp.qty * qty; - let itemId: string | null = null; - if (key) { - const it = await findItemByKey(guildId, key); - itemId = it?.id ?? null; - } else if (comp.itemId) { - itemId = comp.itemId; - } - if (!itemId) continue; - await prisma.inventoryEntry.update({ - where: { userId_guildId_itemId: { userId, guildId, itemId } }, - data: { quantity: { decrement: compQty } }, - }); - } - } - - // Entregar producto - const item = await prisma.economyItem.findUnique({ - where: { id: offer.itemId }, - }); - if (!item) throw new Error("Ítem de oferta no existente"); - await addItemByKey(userId, guildId, item.key, qty); - - // Registrar compra - await prisma.shopPurchase.create({ - data: { offerId: offer.id, userId, guildId, qty }, - }); - - // Reducir stock global - if (offer.stock != null) { - await prisma.shopOffer.update({ - where: { id: offer.id }, - data: { stock: offer.stock - qty }, - }); - } - - return { ok: true, item, qty } as const; -} - -// --------------------------- -// Mutaciones -// --------------------------- -export async function findMutationByKey(guildId: string, key: string) { - return prisma.itemMutation.findFirst({ - where: { key, OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); -} - -export async function applyMutationToInventory( - userId: string, - guildId: string, - itemKey: string, - mutationKey: string -) { - const { item, entry } = await getInventoryEntry(userId, guildId, itemKey, { - createIfMissing: true, - }); - if (!entry) throw new Error("Inventario inexistente"); - - // Política de mutaciones - const props = parseItemProps(item.props); - const policy = props.mutationPolicy; - if (policy?.deniedKeys?.includes(mutationKey)) - throw new Error("Mutación denegada"); - if (policy?.allowedKeys && !policy.allowedKeys.includes(mutationKey)) - throw new Error("Mutación no permitida"); - - const mutation = await findMutationByKey(guildId, mutationKey); - if (!mutation) throw new Error("Mutación no encontrada"); - - // Registrar vínculo - await prisma.inventoryItemMutation.create({ - data: { inventoryId: entry.id, mutationId: mutation.id }, - }); - return { ok: true } as const; -} diff --git a/src/.backup/game_core/game/economy/types.ts b/src/.backup/game_core/game/economy/types.ts deleted file mode 100644 index 6034472..0000000 --- a/src/.backup/game_core/game/economy/types.ts +++ /dev/null @@ -1,133 +0,0 @@ -// Tipos para la capa JSON flexible de economía. -// Estos tipos NO fuerzan el esquema en base de datos, solo sirven para dar seguridad de tipos en el código. - -export type PriceItemComponent = { - itemKey?: string; // preferido para lookup - itemId?: string; // fallback directo - qty: number; -}; - -export type Price = { - coins?: number; - items?: PriceItemComponent[]; - extra?: Record; -}; - -export type ChestReward = - | { type: "coins"; amount: number; probability?: number } - | { - type: "item"; - itemKey?: string; - itemId?: string; - qty: number; - probability?: number; - } - | { type: "role"; roleId: string; probability?: number }; - -export type PassiveEffect = { - key: string; // p.ej. "xpBoost", "defenseUp" - value?: unknown; // libre según tu lógica - expiresAt?: string; // ISO string opcional -}; - -export type BreakableProps = { - enabled?: boolean; - // Para ítems no apilables, cada instancia puede tener durabilidad independiente - maxDurability?: number; - // Opcional: cuánto se reduce por uso/acción - durabilityPerUse?: number; -}; - -export type CraftableProps = { - enabled?: boolean; -}; - -export type ChestProps = { - enabled?: boolean; - // Modo de randomización: - // 'all' (default): se procesan todas las recompensas y cada una evalúa su probability (si no hay probability, se asume 100%). - // 'single': selecciona UNA recompensa aleatoria ponderada por probability (o 1 si falta) y solo otorga esa. - // 'roll-each': similar a 'all' pero probability se trata como chance independiente (igual que all; se mantiene por semántica futura). - randomMode?: "all" | "single" | "roll-each"; - // Recompensas configuradas - rewards?: ChestReward[]; - // Roles adicionales fijos (independientes de rewards) - roles?: string[]; - // Si true, consume 1 del inventario al abrir - consumeOnOpen?: boolean; -}; - -export type EventCurrencyProps = { - enabled?: boolean; - eventKey?: string; // Identificador del evento -}; - -export type MutationPolicy = { - // Lista blanca/negra para mutaciones permitidas - allowedKeys?: string[]; - deniedKeys?: string[]; -}; - -export type ShopProps = { - purchasable?: boolean; // si puede venderse en la tienda (además de ShopOffer) -}; - -export type ToolProps = { - type: "pickaxe" | "rod" | "sword" | "bow" | "halberd" | "net" | string; // extensible - tier?: number; // nivel/calidad de la herramienta -}; - -export type FoodProps = { - healHp?: number; // sanación plana - healPercent?: number; // sanación porcentual del maxHp - cooldownKey?: string; // clave de cooldown personalizada - cooldownSeconds?: number; // cd para volver a usar -}; - -export type ItemProps = { - // Flags y bloques de config opcionales - breakable?: BreakableProps; // romperse - craftable?: CraftableProps; // craftear - chest?: ChestProps; // estilo cofre que al usar da roles/ítems/monedas - // Si true, este ítem se considera global (guildId = null) y solo el owner del bot puede editarlo - global?: boolean; - eventCurrency?: EventCurrencyProps; // puede actuar como moneda de evento - passiveEffects?: PassiveEffect[]; // efectos por tenerlo - mutationPolicy?: MutationPolicy; // reglas para mutaciones extra - craftingOnly?: boolean; // ítem que solo sirve para craftear (p.ej. mineral) - availableFrom?: string; // ISO para adquirir/usar si deseas sobreescribir a nivel props - availableTo?: string; - usableFrom?: string; - usableTo?: string; - shop?: ShopProps; // metadatos de tienda - tool?: ToolProps; // metadatos de herramienta (pico, caña, espada, etc.) - food?: FoodProps; // metadatos de comida/poción (curación, cooldown) - // Stats básicos de combate (opcionales) - damage?: number; // para armas - defense?: number; // para armaduras - maxHpBonus?: number; // para capas u otros - // Cualquier otra extensión libre - [k: string]: unknown; -}; - -// Estado por entrada de inventario (InventoryEntry.state) -export type InventoryState = { - // Para ítems no apilables (stackable=false), puedes manejar varias instancias con durabilidad/expiración por unidad - instances?: Array<{ - durability?: number; - expiresAt?: string; // ISO - notes?: string; - // Mutaciones aplicadas a esta instancia concreta - mutations?: string[]; // mutation keys, o ids si prefieres - }>; - // Campo libre para tus necesidades - notes?: string; - [k: string]: unknown; -}; - -export type OpenChestResult = { - coinsDelta: number; - itemsToAdd: Array<{ itemKey?: string; itemId?: string; qty: number }>; - rolesToGrant: string[]; // IDs de roles a otorgar - consumed: boolean; // si el ítem/cofre se consumió -}; diff --git a/src/.backup/game_core/game/lib/rpgFormat.ts b/src/.backup/game_core/game/lib/rpgFormat.ts deleted file mode 100644 index 6b2faa8..0000000 --- a/src/.backup/game_core/game/lib/rpgFormat.ts +++ /dev/null @@ -1,131 +0,0 @@ -// Utilidades de formato visual estilo RPG para comandos y resúmenes. -// Centraliza lógica repetida (barras de corazones, durabilidad, etiquetas de herramientas). - -export function heartsBar( - current: number, - max: number, - opts?: { segments?: number; fullChar?: string; emptyChar?: string } -) { - const segments = opts?.segments ?? Math.min(20, max); // límite visual - const full = opts?.fullChar ?? "❤"; - const empty = opts?.emptyChar ?? "♡"; - const clampedMax = Math.max(1, max); - const ratio = current / clampedMax; - const filled = Math.max(0, Math.min(segments, Math.round(ratio * segments))); - return full.repeat(filled) + empty.repeat(segments - filled); -} - -export function durabilityBar(remaining: number, max: number, segments = 10) { - const safeMax = Math.max(1, max); - const ratio = Math.max(0, Math.min(1, remaining / safeMax)); - const filled = Math.round(ratio * segments); - const bar = Array.from({ length: segments }) - .map((_, i) => (i < filled ? "█" : "░")) - .join(""); - return `[${bar}] ${Math.max(0, remaining)}/${safeMax}`; -} - -export function formatToolLabel(params: { - key: string; - displayName: string; - instancesRemaining?: number | null; - broken?: boolean; - brokenInstance?: boolean; - durabilityDelta?: number | null; - remaining?: number | null; - max?: number | null; - source?: string | null; - fallbackIcon?: string; -}) { - const { - key, - displayName, - instancesRemaining, - broken, - brokenInstance, - durabilityDelta, - remaining, - max, - source, - fallbackIcon = "🔧", - } = params; - - const multi = - instancesRemaining && instancesRemaining > 1 - ? ` (x${instancesRemaining})` - : ""; - const base = `${displayName || key}${multi}`; - let status = ""; - if (broken) status = " (agotada)"; - else if (brokenInstance) - status = ` (instancia rota, quedan ${instancesRemaining})`; - const delta = durabilityDelta != null ? ` (-${durabilityDelta} dur.)` : ""; - const dur = - remaining != null && max != null - ? `\nDurabilidad: ${durabilityBar(remaining, max)}` - : ""; - const src = source ? ` \`(${source})\`` : ""; - return `${base}${status}${delta}${src}${dur}`; -} - -export function outcomeLabel(outcome?: "victory" | "defeat") { - if (!outcome) return ""; - return outcome === "victory" ? "🏆 Victoria" : "💀 Derrota"; -} - -export function combatSummaryRPG(c: { - mobs: number; - mobsDefeated: number; - totalDamageDealt: number; - totalDamageTaken: number; - playerStartHp?: number | null; - playerEndHp?: number | null; - outcome?: "victory" | "defeat"; - maxRefHp?: number; // para cálculo visual si difiere - autoDefeatNoWeapon?: boolean; - deathPenalty?: { - goldLost?: number; - fatigueAppliedMinutes?: number; - fatigueMagnitude?: number; - percentApplied?: number; - }; -}) { - const header = `**Combate (${outcomeLabel(c.outcome)})**`; - const lines = [ - `• Mobs: ${c.mobs} | Derrotados: ${c.mobsDefeated}/${c.mobs}`, - `• Daño hecho: ${c.totalDamageDealt} | Daño recibido: ${c.totalDamageTaken}`, - ]; - if (c.autoDefeatNoWeapon) { - lines.push( - `• Derrota automática: no tenías arma equipada o válida (daño 0). Equipa un arma para poder atacar.` - ); - } - if (c.deathPenalty) { - const parts: string[] = []; - if ( - typeof c.deathPenalty.goldLost === "number" && - c.deathPenalty.goldLost > 0 - ) - parts.push(`-${c.deathPenalty.goldLost} monedas`); - if (c.deathPenalty.fatigueAppliedMinutes) { - const pct = c.deathPenalty.fatigueMagnitude - ? Math.round(c.deathPenalty.fatigueMagnitude * 100) - : 15; - parts.push(`Fatiga ${pct}% ${c.deathPenalty.fatigueAppliedMinutes}m`); - } - if (typeof c.deathPenalty.percentApplied === "number") { - parts.push(`(${Math.round(c.deathPenalty.percentApplied * 100)}% oro)`); - } - if (parts.length) lines.push(`• Penalización: ${parts.join(" | ")}`); - } - if (c.playerStartHp != null && c.playerEndHp != null) { - const maxHp = c.maxRefHp || Math.max(c.playerStartHp, c.playerEndHp); - lines.push( - `• HP: ${c.playerStartHp} → ${c.playerEndHp} ${heartsBar( - c.playerEndHp, - maxHp - )}` - ); - } - return `${header}\n${lines.join("\n")}`; -} diff --git a/src/.backup/game_core/game/lib/toolBreakLog.ts b/src/.backup/game_core/game/lib/toolBreakLog.ts deleted file mode 100644 index cba67a2..0000000 --- a/src/.backup/game_core/game/lib/toolBreakLog.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Logger en memoria de rupturas de herramientas. -// Reemplazable en el futuro por una tabla ToolBreakLog. - -export interface ToolBreakEvent { - ts: number; - userId: string; - guildId: string; - toolKey: string; - brokenInstance: boolean; // true si fue una instancia, false si se agotó totalmente (última) - instancesRemaining: number; -} - -const MAX_EVENTS = 200; -const buffer: ToolBreakEvent[] = []; - -export function logToolBreak(ev: ToolBreakEvent) { - buffer.unshift(ev); - if (buffer.length > MAX_EVENTS) buffer.pop(); -} - -export function getToolBreaks( - limit = 20, - guildFilter?: string, - userFilter?: string -) { - return buffer - .filter( - (e) => - (!guildFilter || e.guildId === guildFilter) && - (!userFilter || e.userId === userFilter) - ) - .slice(0, limit); -} diff --git a/src/.backup/game_core/game/minigames/demoRun.ts b/src/.backup/game_core/game/minigames/demoRun.ts deleted file mode 100644 index 360e75c..0000000 --- a/src/.backup/game_core/game/minigames/demoRun.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { prisma } from '../../core/database/prisma'; -import { addItemByKey } from '../economy/service'; -import { runMinigame } from './service'; - -async function main() { - const userId = process.env.TEST_USER_ID; - const guildId = process.env.TEST_GUILD_ID || 'test-guild'; - if (!userId) throw new Error('Set TEST_USER_ID in env'); - - // Ensure User and Guild exist logically for foreign keys if you enforce them in app - // Here we just make sure they exist in DB if needed. - await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId, name: 'Test Guild' } }); - await prisma.user.upsert({ where: { id: userId }, update: {}, create: { id: userId } }); - - // Ensure player has a basic pickaxe - await addItemByKey(userId, guildId, 'tool.pickaxe.basic', 1); - - // Run mining level 1 - const result = await runMinigame(userId, guildId, 'mine.cavern', 1, { toolKey: 'tool.pickaxe.basic' }); - console.log('[demo:minigame] result:', JSON.stringify(result)); -} - -main().then(() => process.exit(0)).catch((e) => { console.error(e); process.exit(1); }); - diff --git a/src/.backup/game_core/game/minigames/seed.ts b/src/.backup/game_core/game/minigames/seed.ts deleted file mode 100644 index fd0c2d5..0000000 --- a/src/.backup/game_core/game/minigames/seed.ts +++ /dev/null @@ -1,674 +0,0 @@ -import { prisma } from "../../core/database/prisma"; -import { Prisma } from "@prisma/client"; - -async function upsertEconomyItem( - guildId: string | null, - key: string, - data: Omit -) { - if (guildId) { - return prisma.economyItem.upsert({ - where: { guildId_key: { guildId, key } }, - update: {}, - create: { ...data, key, guildId }, - }); - } - const existing = await prisma.economyItem.findFirst({ - where: { key, guildId: null }, - }); - if (existing) - return prisma.economyItem.update({ where: { id: existing.id }, data: {} }); - return prisma.economyItem.create({ data: { ...data, key, guildId: null } }); -} - -async function upsertGameArea( - guildId: string | null, - key: string, - data: Omit -) { - if (guildId) { - return prisma.gameArea.upsert({ - where: { guildId_key: { guildId, key } }, - update: {}, - create: { ...data, key, guildId }, - }); - } - const existing = await prisma.gameArea.findFirst({ - where: { key, guildId: null }, - }); - if (existing) - return prisma.gameArea.update({ where: { id: existing.id }, data: {} }); - return prisma.gameArea.create({ data: { ...data, key, guildId: null } }); -} - -async function upsertMob( - guildId: string | null, - key: string, - data: Omit -) { - if (guildId) { - return prisma.mob.upsert({ - where: { guildId_key: { guildId, key } }, - update: { - stats: (data as any).stats, - drops: (data as any).drops, - name: (data as any).name, - }, - create: { ...data, key, guildId }, - }); - } - const existing = await prisma.mob.findFirst({ - where: { key, guildId: null }, - }); - if (existing) - return prisma.mob.update({ - where: { id: existing.id }, - data: { - stats: (data as any).stats, - drops: (data as any).drops, - name: (data as any).name, - }, - }); - return prisma.mob.create({ data: { ...data, key, guildId: null } }); -} - -async function main() { - const guildId = "1316592320954630144"; // null => global - - // Items base: herramientas y minerales - const pickKey = "tool.pickaxe.basic"; - const rodKey = "tool.rod.basic"; - const swordKey = "weapon.sword.iron"; - const armorKey = "armor.leather.basic"; - const capeKey = "cape.life.minor"; - - const ironKey = "ore.iron"; - const goldKey = "ore.gold"; - const ironIngotKey = "ingot.iron"; - - const fishCommonKey = "fish.common"; - const fishRareKey = "fish.rare"; - - // Herramientas - await upsertEconomyItem(guildId, pickKey, { - name: "Pico Básico", - stackable: false, - props: { - tool: { type: "pickaxe", tier: 1 }, - breakable: { enabled: true, maxDurability: 100, durabilityPerUse: 5 }, - } as unknown as Prisma.InputJsonValue, - tags: ["tool", "mine"], - }); - - await upsertEconomyItem(guildId, rodKey, { - name: "Caña Básica", - stackable: false, - props: { - tool: { type: "rod", tier: 1 }, - breakable: { enabled: true, maxDurability: 80, durabilityPerUse: 3 }, - } as unknown as Prisma.InputJsonValue, - tags: ["tool", "fish"], - }); - - // Arma, armadura y capa - await upsertEconomyItem(guildId, swordKey, { - name: "Espada de Hierro", - stackable: false, - props: { - damage: 10, - tool: { type: "sword", tier: 1 }, - breakable: { enabled: true, maxDurability: 150, durabilityPerUse: 2 }, - } as unknown as Prisma.InputJsonValue, - tags: ["weapon"], - }); - - await upsertEconomyItem(guildId, armorKey, { - name: "Armadura de Cuero", - stackable: false, - props: { defense: 3 } as unknown as Prisma.InputJsonValue, - tags: ["armor"], - }); - - await upsertEconomyItem(guildId, capeKey, { - name: "Capa de Vida Menor", - stackable: false, - props: { maxHpBonus: 20 } as unknown as Prisma.InputJsonValue, - tags: ["cape"], - }); - - // Materiales - await upsertEconomyItem(guildId, ironKey, { - name: "Mineral de Hierro", - stackable: true, - props: { craftingOnly: true } as unknown as Prisma.InputJsonValue, - tags: ["ore", "common"], - }); - - await upsertEconomyItem(guildId, goldKey, { - name: "Mineral de Oro", - stackable: true, - props: { craftingOnly: true } as unknown as Prisma.InputJsonValue, - tags: ["ore", "rare"], - }); - - await upsertEconomyItem(guildId, ironIngotKey, { - name: "Lingote de Hierro", - stackable: true, - props: {} as unknown as Prisma.InputJsonValue, - tags: ["ingot", "metal"], - }); - - // Comida (pesca) que cura con cooldown - await upsertEconomyItem(guildId, fishCommonKey, { - name: "Pez Común", - stackable: true, - props: { - food: { healHp: 10, cooldownSeconds: 30 }, - } as unknown as Prisma.InputJsonValue, - tags: ["fish", "food", "common"], - }); - - await upsertEconomyItem(guildId, fishRareKey, { - name: "Pez Raro", - stackable: true, - props: { - food: { healHp: 20, healPercent: 5, cooldownSeconds: 45 }, - } as unknown as Prisma.InputJsonValue, - tags: ["fish", "food", "rare"], - }); - - // Área de mina con niveles - const mineArea = await upsertGameArea(guildId, "mine.cavern", { - name: "Mina: Caverna", - type: "MINE", - config: { cooldownSeconds: 10 } as unknown as Prisma.InputJsonValue, - }); - - await prisma.gameAreaLevel.upsert({ - where: { areaId_level: { areaId: mineArea.id, level: 1 } }, - update: {}, - create: { - areaId: mineArea.id, - level: 1, - requirements: { - tool: { required: true, toolType: "pickaxe", minTier: 1 }, - } as unknown as Prisma.InputJsonValue, - rewards: { - draws: 2, - table: [ - { type: "item", itemKey: ironKey, qty: 2, weight: 70 }, - { type: "item", itemKey: ironKey, qty: 3, weight: 20 }, - { type: "item", itemKey: goldKey, qty: 1, weight: 10 }, - ], - } as unknown as Prisma.InputJsonValue, - mobs: { - draws: 1, - table: [ - { mobKey: "bat", weight: 20 }, - { mobKey: "slime", weight: 10 }, - ], - } as unknown as Prisma.InputJsonValue, - }, - }); - - await prisma.gameAreaLevel.upsert({ - where: { areaId_level: { areaId: mineArea.id, level: 2 } }, - update: {}, - create: { - areaId: mineArea.id, - level: 2, - requirements: { - tool: { required: true, toolType: "pickaxe", minTier: 2 }, - } as unknown as Prisma.InputJsonValue, - rewards: { - draws: 3, - table: [ - { type: "item", itemKey: ironKey, qty: 3, weight: 60 }, - { type: "item", itemKey: goldKey, qty: 1, weight: 30 }, - { type: "coins", amount: 50, weight: 10 }, - ], - } as unknown as Prisma.InputJsonValue, - mobs: { - draws: 2, - table: [ - { mobKey: "bat", weight: 20 }, - { mobKey: "slime", weight: 20 }, - { mobKey: "goblin", weight: 10 }, - ], - } as unknown as Prisma.InputJsonValue, - }, - }); - - // Área de laguna (pesca) - const lagoon = await upsertGameArea(guildId, "lagoon.shore", { - name: "Laguna: Orilla", - type: "LAGOON", - config: { cooldownSeconds: 12 } as unknown as Prisma.InputJsonValue, - }); - - await prisma.gameAreaLevel.upsert({ - where: { areaId_level: { areaId: lagoon.id, level: 1 } }, - update: {}, - create: { - areaId: lagoon.id, - level: 1, - requirements: { - tool: { required: true, toolType: "rod", minTier: 1 }, - } as unknown as Prisma.InputJsonValue, - rewards: { - draws: 2, - table: [ - { type: "item", itemKey: fishCommonKey, qty: 1, weight: 70 }, - { type: "item", itemKey: fishRareKey, qty: 1, weight: 10 }, - { type: "coins", amount: 10, weight: 20 }, - ], - } as unknown as Prisma.InputJsonValue, - mobs: { draws: 0, table: [] } as unknown as Prisma.InputJsonValue, - }, - }); - - // Área de pelea (arena) - const arena = await upsertGameArea(guildId, "fight.arena", { - name: "Arena de Combate", - type: "FIGHT", - config: { cooldownSeconds: 15 } as unknown as Prisma.InputJsonValue, - }); - - await prisma.gameAreaLevel.upsert({ - where: { areaId_level: { areaId: arena.id, level: 1 } }, - update: {}, - create: { - areaId: arena.id, - level: 1, - requirements: { - tool: { - required: true, - toolType: "sword", - minTier: 1, - allowedKeys: [swordKey], - }, - } as unknown as Prisma.InputJsonValue, - rewards: { - draws: 1, - table: [{ type: "coins", amount: 25, weight: 100 }], - } as unknown as Prisma.InputJsonValue, - mobs: { - draws: 1, - table: [ - { mobKey: "slime", weight: 50 }, - { mobKey: "goblin", weight: 50 }, - ], - } as unknown as Prisma.InputJsonValue, - }, - }); - - // Mobs básicos - const mobs = [ - { key: "bat", name: "Murciélago", stats: { attack: 4 } }, - { key: "slime", name: "Slime", stats: { attack: 6 } }, - { key: "goblin", name: "Duende", stats: { attack: 8 } }, - ]; - for (const m of mobs) { - await upsertMob(guildId, m.key, { - name: m.name, - stats: m.stats as unknown as Prisma.InputJsonValue, - drops: Prisma.DbNull, - }); - } - - // Programar un par de ataques de mobs (demostración) - const targetUser = "327207082203938818"; - if (targetUser) { - const slime = await prisma.mob.findFirst({ - where: { key: "slime", OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); - if (slime) { - const now = Date.now(); - await prisma.scheduledMobAttack.createMany({ - data: [ - { - userId: targetUser, - guildId: guildId ?? "global", - mobId: slime.id, - scheduleAt: new Date(now + 5_000), - }, - { - userId: targetUser, - guildId: guildId ?? "global", - mobId: slime.id, - scheduleAt: new Date(now + 15_000), - }, - ], - }); - } - } - - // --------------------------------------------------------------------------- - // NUEVO CONTENIDO PARA PROBAR SISTEMAS AVANZADOS (tiers, riskFactor, fatiga) - // --------------------------------------------------------------------------- - - // Herramientas / equipo Tier 2 - const pickKeyT2 = "tool.pickaxe.iron"; - const rodKeyT2 = "tool.rod.oak"; - const swordKeyT2 = "weapon.sword.steel"; - const armorKeyT2 = "armor.chain.basic"; - const capeKeyT2 = "cape.life.moderate"; - - await upsertEconomyItem(guildId, pickKeyT2, { - name: "Pico de Hierro", - stackable: false, - props: { - tool: { type: "pickaxe", tier: 2 }, - breakable: { enabled: true, maxDurability: 180, durabilityPerUse: 4 }, - } as unknown as Prisma.InputJsonValue, - tags: ["tool", "mine", "tier2"], - }); - - await upsertEconomyItem(guildId, rodKeyT2, { - name: "Caña Robusta", - stackable: false, - props: { - tool: { type: "rod", tier: 2 }, - breakable: { enabled: true, maxDurability: 140, durabilityPerUse: 3 }, - } as unknown as Prisma.InputJsonValue, - tags: ["tool", "fish", "tier2"], - }); - - await upsertEconomyItem(guildId, swordKeyT2, { - name: "Espada de Acero", - stackable: false, - props: { - damage: 18, - tool: { type: "sword", tier: 2 }, - breakable: { enabled: true, maxDurability: 220, durabilityPerUse: 2 }, - } as unknown as Prisma.InputJsonValue, - tags: ["weapon", "tier2"], - }); - - await upsertEconomyItem(guildId, armorKeyT2, { - name: "Armadura de Cota de Malla", - stackable: false, - props: { defense: 6 } as unknown as Prisma.InputJsonValue, - tags: ["armor", "tier2"], - }); - - await upsertEconomyItem(guildId, capeKeyT2, { - name: "Capa de Vida Moderada", - stackable: false, - props: { maxHpBonus: 40 } as unknown as Prisma.InputJsonValue, - tags: ["cape", "tier2"], - }); - - // Consumibles / pruebas de curación y limpieza de efectos - const bigFoodKey = "food.meat.large"; - const fatigueClearPotionKey = "potion.fatigue.clear"; - - await upsertEconomyItem(guildId, bigFoodKey, { - name: "Carne Asada Grande", - stackable: true, - props: { - food: { healHp: 40, healPercent: 10, cooldownSeconds: 60 }, - } as unknown as Prisma.InputJsonValue, - tags: ["food", "healing"], - }); - - await upsertEconomyItem(guildId, fatigueClearPotionKey, { - name: "Poción Energética", - stackable: true, - props: { - potion: { removeEffects: ["FATIGUE"], cooldownSeconds: 90 }, - } as unknown as Prisma.InputJsonValue, - tags: ["potion", "utility"], - }); - - // ÁREA NUEVA: Mina de Fisura (más riesgo => probar penalización muerte) - const riftMine = await upsertGameArea(guildId, "mine.rift", { - name: "Mina: Fisura Cristalina", - type: "MINE", - config: { cooldownSeconds: 14 } as unknown as Prisma.InputJsonValue, - metadata: { riskFactor: 1.6 } as unknown as Prisma.InputJsonValue, - }); - - await prisma.gameAreaLevel.upsert({ - where: { areaId_level: { areaId: riftMine.id, level: 1 } }, - update: {}, - create: { - areaId: riftMine.id, - level: 1, - requirements: { - tool: { required: true, toolType: "pickaxe", minTier: 2 }, - } as unknown as Prisma.InputJsonValue, - rewards: { - draws: 3, - table: [ - { type: "item", itemKey: ironKey, qty: 4, weight: 55 }, - { type: "item", itemKey: goldKey, qty: 2, weight: 20 }, - { type: "coins", amount: 60, weight: 25 }, - ], - } as unknown as Prisma.InputJsonValue, - mobs: { - draws: 2, - table: [ - { mobKey: "goblin", weight: 25 }, - { mobKey: "orc", weight: 15 }, - ], - } as unknown as Prisma.InputJsonValue, - metadata: { suggestedHp: 120 } as unknown as Prisma.InputJsonValue, - }, - }); - - // Extensión de la mina existente: nivel 3 - await prisma.gameAreaLevel.upsert({ - where: { areaId_level: { areaId: mineArea.id, level: 3 } }, - update: {}, - create: { - areaId: mineArea.id, - level: 3, - requirements: { - tool: { required: true, toolType: "pickaxe", minTier: 2 }, - } as unknown as Prisma.InputJsonValue, - rewards: { - draws: 4, - table: [ - { type: "item", itemKey: ironKey, qty: 4, weight: 50 }, - { type: "item", itemKey: goldKey, qty: 2, weight: 25 }, - { type: "coins", amount: 80, weight: 15 }, - { type: "coins", amount: 120, weight: 10 }, - ], - } as unknown as Prisma.InputJsonValue, - mobs: { - draws: 2, - table: [ - { mobKey: "slime", weight: 20 }, - { mobKey: "goblin", weight: 20 }, - { mobKey: "orc", weight: 10 }, - ], - } as unknown as Prisma.InputJsonValue, - }, - }); - - // Laguna nivel 2 - await prisma.gameAreaLevel.upsert({ - where: { areaId_level: { areaId: lagoon.id, level: 2 } }, - update: {}, - create: { - areaId: lagoon.id, - level: 2, - requirements: { - tool: { required: true, toolType: "rod", minTier: 2 }, - } as unknown as Prisma.InputJsonValue, - rewards: { - draws: 3, - table: [ - { type: "item", itemKey: fishCommonKey, qty: 2, weight: 60 }, - { type: "item", itemKey: fishRareKey, qty: 1, weight: 20 }, - { type: "coins", amount: 30, weight: 20 }, - ], - } as unknown as Prisma.InputJsonValue, - mobs: { draws: 0, table: [] } as unknown as Prisma.InputJsonValue, - }, - }); - - // Arena existente: nivel 2 - await prisma.gameAreaLevel.upsert({ - where: { areaId_level: { areaId: arena.id, level: 2 } }, - update: {}, - create: { - areaId: arena.id, - level: 2, - requirements: { - tool: { required: true, toolType: "sword", minTier: 2 }, - } as unknown as Prisma.InputJsonValue, - rewards: { - draws: 1, - table: [ - { type: "coins", amount: 60, weight: 70 }, - { type: "coins", amount: 90, weight: 30 }, - ], - } as unknown as Prisma.InputJsonValue, - mobs: { - draws: 1, - table: [ - { mobKey: "goblin", weight: 40 }, - { mobKey: "orc", weight: 40 }, - { mobKey: "troll", weight: 20 }, - ], - } as unknown as Prisma.InputJsonValue, - }, - }); - - // Arena élite separada para probar riskFactor de muerte - const eliteArena = await upsertGameArea(guildId, "fight.arena.elite", { - name: "Arena de Combate Élite", - type: "FIGHT", - config: { cooldownSeconds: 25 } as unknown as Prisma.InputJsonValue, - metadata: { riskFactor: 1.4 } as unknown as Prisma.InputJsonValue, - }); - - await prisma.gameAreaLevel.upsert({ - where: { areaId_level: { areaId: eliteArena.id, level: 1 } }, - update: {}, - create: { - areaId: eliteArena.id, - level: 1, - requirements: { - tool: { required: true, toolType: "sword", minTier: 2 }, - } as unknown as Prisma.InputJsonValue, - rewards: { - draws: 1, - table: [ - { type: "coins", amount: 120, weight: 70 }, - { type: "coins", amount: 180, weight: 30 }, - ], - } as unknown as Prisma.InputJsonValue, - mobs: { - draws: 1, - table: [ - { mobKey: "orc", weight: 40 }, - { mobKey: "troll", weight: 35 }, - { mobKey: "dragonling", weight: 25 }, - ], - } as unknown as Prisma.InputJsonValue, - metadata: { suggestedHp: 150 } as unknown as Prisma.InputJsonValue, - }, - }); - - // Nuevos mobs avanzados - const extraMobs = [ - { key: "orc", name: "Orco", stats: { attack: 12, defense: 2 } }, - { key: "troll", name: "Trol", stats: { attack: 20, defense: 4 } }, - { - key: "dragonling", - name: "Dragoncito", - stats: { attack: 35, defense: 6 }, - }, - ]; - for (const m of extraMobs) { - await upsertMob(guildId, m.key, { - name: m.name, - stats: m.stats as unknown as Prisma.InputJsonValue, - drops: Prisma.DbNull, - }); - } - - // Programar ataques extra de mobs nuevos para pruebas (si existe user objetivo) - if (targetUser) { - const orc = await prisma.mob.findFirst({ - where: { key: "orc", OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); - const dragon = await prisma.mob.findFirst({ - where: { key: "dragonling", OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); - const now = Date.now(); - const extraAttacks: Prisma.ScheduledMobAttackCreateManyInput[] = []; - if (orc) { - extraAttacks.push({ - userId: targetUser, - guildId: guildId ?? "global", - mobId: orc.id, - scheduleAt: new Date(now + 25_000), - }); - } - if (dragon) { - extraAttacks.push({ - userId: targetUser, - guildId: guildId ?? "global", - mobId: dragon.id, - scheduleAt: new Date(now + 40_000), - }); - } - if (extraAttacks.length) { - await prisma.scheduledMobAttack.createMany({ data: extraAttacks }); - } - } - - // Insertar un efecto FATIGUE de prueba (15% por 30 min) para validar penalización de monedas y reducción de stats - if (targetUser) { - const expires = new Date(Date.now() + 30 * 60 * 1000); - await prisma.playerStatusEffect.upsert({ - where: { - userId_guildId_type: { - userId: targetUser, - guildId: guildId ?? "global", - type: "FATIGUE", - }, - }, - update: { magnitude: 0.15, expiresAt: expires }, - create: { - userId: targetUser, - guildId: guildId ?? "global", - type: "FATIGUE", - magnitude: 0.15, - expiresAt: expires, - }, - }); - } - - // Asegurar PlayerState base para el usuario de prueba - if (targetUser) { - await prisma.playerState.upsert({ - where: { - userId_guildId: { userId: targetUser, guildId: guildId ?? "global" }, - }, - update: {}, - create: { - userId: targetUser, - guildId: guildId ?? "global", - hp: 100, - maxHp: 100, - }, - }); - } - - console.log("[seed:minigames] done"); -} - -main() - .then(() => process.exit(0)) - .catch((e) => { - console.error(e); - process.exit(1); - }); diff --git a/src/.backup/game_core/game/minigames/service.ts b/src/.backup/game_core/game/minigames/service.ts deleted file mode 100644 index 387236b..0000000 --- a/src/.backup/game_core/game/minigames/service.ts +++ /dev/null @@ -1,1165 +0,0 @@ -import { - applyDeathFatigue, - getActiveStatusEffects, -} from "../combat/statusEffectsService"; -import { getOrCreateWallet } from "../economy/service"; -import { prisma } from "../../core/database/prisma"; -import { - addItemByKey, - adjustCoins, - findItemByKey, - getInventoryEntry, -} from "../economy/service"; -import { findMobDef } from "../mobs/mobData"; -import { - getEffectiveStats, - adjustHP, - ensurePlayerState, - getEquipment, -} from "../combat/equipmentService"; // 🟩 local authoritative -import { logToolBreak } from "../lib/toolBreakLog"; -import { updateStats } from "../stats/service"; // 🟩 local authoritative -import type { ItemProps, InventoryState } from "../economy/types"; -import { parseItemProps, parseState as parseInvState } from "../core/utils"; -import type { - LevelRequirements, - RunMinigameOptions, - RunResult, - RewardsTable, - MobsTable, - CombatSummary, -} from "./types"; -import type { Prisma } from "@prisma/client"; - -// Escalado dinámico de penalización por derrota según área/nivel y riesgo. -// Se puede ampliar leyendo area.metadata.riskFactor (0-3) y level. -function computeDeathPenaltyPercent( - area: { key: string; metadata: any }, - level: number -): number { - const meta = (area.metadata as any) || {}; - const base = 0.05; // 5% base - const risk = - typeof meta.riskFactor === "number" - ? Math.max(0, Math.min(3, meta.riskFactor)) - : 0; - const levelBoost = Math.min(0.1, Math.max(0, (level - 1) * 0.005)); // +0.5% por nivel adicional hasta +10% - const riskBoost = risk * 0.02; // cada punto riesgo +2% - let pct = base + levelBoost + riskBoost; - if (pct > 0.25) pct = 0.25; // cap 25% - return pct; // ej: 0.08 = 8% -} - -// Auto-select best tool from inventory by type and constraints -async function findBestToolKey( - userId: string, - guildId: string, - toolType: string, - opts?: { minTier?: number; allowedKeys?: string[] } -) { - const entries = await prisma.inventoryEntry.findMany({ - where: { userId, guildId, quantity: { gt: 0 } }, - include: { item: true }, - }); - let best: { key: string; tier: number; isPrimaryTool: boolean } | null = null; - for (const e of entries) { - const props = parseItemProps(e.item.props); - const t = props.tool; - if (!t || t.type !== toolType) continue; - const tier = Math.max(0, t.tier ?? 0); - if (opts?.minTier != null && tier < opts.minTier) continue; - if ( - opts?.allowedKeys && - opts.allowedKeys.length && - !opts.allowedKeys.includes(e.item.key) - ) - continue; - // Priorizar items con key que comience con "tool." (herramientas primarias) - // sobre armas que también tienen toolType (ej: espada con tool.type:sword) - const isPrimaryTool = e.item.key.startsWith("tool."); - if ( - !best || - tier > best.tier || - (tier === best.tier && isPrimaryTool && !best.isPrimaryTool) - ) { - best = { key: e.item.key, tier, isPrimaryTool }; - } - } - return best?.key ?? null; -} - -function parseJSON(v: unknown): T | null { - if (!v || (typeof v !== "object" && typeof v !== "string")) return null; - return v as T; -} - -function pickWeighted(arr: T[]): T | null { - const total = arr.reduce((s, a) => s + Math.max(0, a.weight || 0), 0); - if (total <= 0) return null; - const r = Math.random() * total; - let acc = 0; - for (const a of arr) { - acc += Math.max(0, a.weight || 0); - if (r <= acc) return a; - } - return arr[arr.length - 1] ?? null; -} - -async function ensureAreaAndLevel( - guildId: string, - areaKey: string, - level: number -) { - const area = await prisma.gameArea.findFirst({ - where: { key: areaKey, OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); - if (!area) throw new Error("Área no encontrada"); - const lvl = await prisma.gameAreaLevel.findFirst({ - where: { areaId: area.id, level }, - }); - if (!lvl) throw new Error("Nivel no encontrado"); - return { area, lvl } as const; -} - -// parseItemProps y parseInvState son importados desde ../core/utils para centralizar parsing - -async function validateRequirements( - userId: string, - guildId: string, - req?: LevelRequirements, - toolKey?: string -) { - if (!req) - return { - toolKeyUsed: undefined as string | undefined, - toolSource: undefined as "provided" | "equipped" | "auto" | undefined, - }; - const toolReq = req.tool; - if (!toolReq) - return { - toolKeyUsed: undefined as string | undefined, - toolSource: undefined, - }; - - let toolKeyUsed = toolKey; - let toolSource: "provided" | "equipped" | "auto" | undefined = undefined; - if (toolKeyUsed) toolSource = "provided"; - - // Auto-select tool when required and not provided - if (!toolKeyUsed && toolReq.required && toolReq.toolType) { - // 1. Intentar herramienta equipada en slot weapon si coincide el tipo - const equip = await prisma.playerEquipment.findUnique({ - where: { userId_guildId: { userId, guildId } }, - }); - if (equip?.weaponItemId) { - const weaponItem = await prisma.economyItem.findUnique({ - where: { id: equip.weaponItemId }, - }); - if (weaponItem) { - const wProps = parseItemProps(weaponItem.props); - if (wProps.tool?.type === toolReq.toolType) { - const tier = Math.max(0, wProps.tool?.tier ?? 0); - if ( - (toolReq.minTier == null || tier >= toolReq.minTier) && - (!toolReq.allowedKeys || - toolReq.allowedKeys.includes(weaponItem.key)) - ) { - toolKeyUsed = weaponItem.key; - toolSource = "equipped"; - } - } - } - } - // 2. Best inventory si no se obtuvo del equipo - if (!toolKeyUsed) { - const best = await findBestToolKey(userId, guildId, toolReq.toolType, { - minTier: toolReq.minTier, - allowedKeys: toolReq.allowedKeys, - }); - if (best) { - toolKeyUsed = best; - toolSource = "auto"; - } - } - } - - // herramienta requerida - if (toolReq.required && !toolKeyUsed) - throw new Error("Se requiere una herramienta adecuada"); - if (!toolKeyUsed) return { toolKeyUsed: undefined, toolSource }; - - // verificar herramienta - const toolItem = await findItemByKey(guildId, toolKeyUsed); - if (!toolItem) throw new Error("Herramienta no encontrada"); - const { entry } = await getInventoryEntry(userId, guildId, toolKeyUsed); - if (!entry || (entry.quantity ?? 0) <= 0) - throw new Error("No tienes la herramienta"); - - const props = parseItemProps(toolItem.props); - const tool = props.tool; - if (toolReq.toolType && tool?.type !== toolReq.toolType) - throw new Error("Tipo de herramienta incorrecto"); - if (toolReq.minTier != null && (tool?.tier ?? 0) < toolReq.minTier) - throw new Error("Tier de herramienta insuficiente"); - if (toolReq.allowedKeys && !toolReq.allowedKeys.includes(toolKeyUsed)) - throw new Error("Esta herramienta no es válida para esta área"); - - return { toolKeyUsed, toolSource }; -} - -async function applyRewards( - userId: string, - guildId: string, - rewards?: RewardsTable -): Promise<{ - rewards: RunResult["rewards"]; - modifiers?: RunResult["rewardModifiers"]; -}> { - const results: RunResult["rewards"] = []; - if (!rewards || !Array.isArray(rewards.table) || rewards.table.length === 0) - return { rewards: results }; - - // Detectar efecto FATIGUE activo para penalizar SOLO monedas. - let fatigueMagnitude: number | undefined; - // prepare a container for merged modifiers so it's available later in the result - let mergedRewardModifiers: RunResult["rewardModifiers"] | undefined; - try { - const effects = await getActiveStatusEffects(userId, guildId); - const fatigue = effects.find((e) => e.type === "FATIGUE"); - if (fatigue && typeof fatigue.magnitude === "number") { - fatigueMagnitude = Math.max(0, Math.min(0.9, fatigue.magnitude)); - } - } catch { - // silencioso - } - const coinMultiplier = fatigueMagnitude - ? Math.max(0, 1 - fatigueMagnitude) - : 1; - - const draws = Math.max(1, rewards.draws ?? 1); - for (let i = 0; i < draws; i++) { - const pick = pickWeighted(rewards.table); - if (!pick) continue; - if (pick.type === "coins") { - const baseAmt = Math.max(0, pick.amount); - if (baseAmt > 0) { - const adjusted = Math.max(0, Math.floor(baseAmt * coinMultiplier)); - const finalAmt = coinMultiplier < 1 && adjusted === 0 ? 1 : adjusted; // al menos 1 si había algo base - if (finalAmt > 0) { - await adjustCoins(userId, guildId, finalAmt); - results.push({ type: "coins", amount: finalAmt }); - } - } - } else if (pick.type === "item") { - const qty = Math.max(1, pick.qty); - await addItemByKey(userId, guildId, pick.itemKey, qty); - results.push({ type: "item", itemKey: pick.itemKey, qty }); - } - } - const modifiers = - coinMultiplier < 1 - ? { fatigueCoinMultiplier: coinMultiplier, fatigueMagnitude } - : undefined; - return { rewards: results, modifiers }; -} - -async function sampleMobs(mobs?: MobsTable): Promise { - const out: string[] = []; - if (!mobs || !Array.isArray(mobs.table) || mobs.table.length === 0) - return out; - const draws = Math.max(0, mobs.draws ?? 0); - for (let i = 0; i < draws; i++) { - const pick = pickWeighted(mobs.table); - if (pick) out.push(pick.mobKey); - } - return out; -} - -// Devuelve instancias de mobs escaladas por nivel (usa getMobInstance de mobData) -import { getMobInstance } from "../mobs/mobData"; - -async function sampleMobInstances( - mobs?: MobsTable, - areaLevel = 1 -): Promise[]> { - const out: ReturnType[] = []; - if (!mobs || !Array.isArray(mobs.table) || mobs.table.length === 0) - return out; - const draws = Math.max(0, mobs.draws ?? 0); - for (let i = 0; i < draws; i++) { - const pick = pickWeighted(mobs.table); - if (!pick) continue; - const inst = getMobInstance(pick.mobKey, areaLevel); - if (inst) out.push(inst); - } - return out; -} - -async function reduceToolDurability( - userId: string, - guildId: string, - toolKey: string, - usage: "gather" | "combat" = "gather" -) { - const { item, entry } = await getInventoryEntry(userId, guildId, toolKey); - if (!entry) - return { - broken: false, - brokenInstance: false, - delta: 0, - remaining: undefined, - max: undefined, - instancesRemaining: 0, - } as const; - const props = parseItemProps(item.props); - const breakable = props.breakable; - // Si el item no es breakable o la durabilidad está deshabilitada, no hacemos nada - if (!breakable || breakable.enabled === false) { - return { - broken: false, - brokenInstance: false, - delta: 0, - remaining: undefined, - max: undefined, - instancesRemaining: entry.quantity ?? 0, - } as const; - } - - // Valores base - const maxConfigured = Math.max(1, breakable.maxDurability ?? 1); - let perUse = Math.max(1, breakable.durabilityPerUse ?? 1); - // Ajuste: en combate degradamos menos para evitar roturas instantáneas de armas caras - if (usage === "combat") { - // Reducimos a la mitad (redondeo hacia arriba mínimo 1) - perUse = Math.max(1, Math.ceil(perUse * 0.5)); - } - - // Protección: si perUse > maxDurability asumimos configuración errónea y lo reducimos a 1 - // (en lugar de romper inmediatamente el ítem). Si quieres que se rompa de un uso, define maxDurability igual a 1. - if (perUse > maxConfigured) perUse = 1; - const delta = perUse; - if (item.stackable) { - // Herramientas deberían ser no apilables; si lo son, solo decrementamos cantidad como fallback - const consumed = Math.min(1, entry.quantity); - let broken = false; - if (consumed > 0) { - const updated = await prisma.inventoryEntry.update({ - where: { userId_guildId_itemId: { userId, guildId, itemId: item.id } }, - data: { quantity: { decrement: consumed } }, - }); - // Consideramos "rota" sólo si después de consumir ya no queda ninguna unidad - broken = (updated.quantity ?? 0) <= 0; - } - return { - broken, - brokenInstance: broken, - delta, - remaining: undefined, - max: maxConfigured, - instancesRemaining: broken ? 0 : (entry.quantity ?? 1) - 1, - } as const; - } - const state = parseInvState(entry.state); - state.instances ??= [{}]; - if (state.instances.length === 0) state.instances.push({}); - - // Seleccionar instancia: ahora usamos la primera, en futuro se puede elegir la de mayor durabilidad restante - const max = maxConfigured; // ya calculado arriba - - // Inicializar durabilidad si no existe (DIRECTO en el array para evitar problemas de referencia) - if (state.instances[0].durability == null) { - state.instances[0].durability = max; - } - - const current = Math.min( - Math.max(0, state.instances[0].durability ?? max), - max - ); - const next = current - delta; - let brokenInstance = false; - - if (next <= 0) { - // romper sólo esta instancia - state.instances.shift(); - brokenInstance = true; - } else { - // Actualizar DIRECTO en el array (no via variable temporal) - state.instances[0].durability = next; - } - - const instancesRemaining = state.instances.length; - const broken = instancesRemaining === 0; // Ítem totalmente agotado - await prisma.inventoryEntry.update({ - where: { userId_guildId_itemId: { userId, guildId, itemId: item.id } }, - data: { - state: state as unknown as Prisma.InputJsonValue, - quantity: state.instances.length, - }, - }); - // Placeholder: logging de ruptura (migrar a ToolBreakLog futuro) - if (brokenInstance) { - logToolBreak({ - ts: Date.now(), - userId, - guildId, - toolKey, - brokenInstance: !broken, // true = solo una instancia - instancesRemaining, - }); - } - return { - broken, - brokenInstance, - delta, - remaining: broken ? 0 : next, - max, - instancesRemaining, - } as const; -} - -export { reduceToolDurability }; - -export async function runMinigame( - userId: string, - guildId: string, - areaKey: string, - level: number, - opts?: RunMinigameOptions -): Promise { - const { area, lvl } = await ensureAreaAndLevel(guildId, areaKey, level); - - // Cooldown por área - const areaConf = (area.config as any) ?? {}; - const cdSeconds = Math.max(0, Number(areaConf.cooldownSeconds ?? 0)); - const cdKey = `minigame:${area.key}`; - if (cdSeconds > 0) { - const existing = await prisma.actionCooldown.findUnique({ - where: { userId_guildId_key: { userId, guildId, key: cdKey } }, - }); - if (existing && existing.until > new Date()) { - throw new Error("Cooldown activo para esta actividad"); - } - } - - // Leer configuración de nivel (requirements, rewards, mobs) - const requirements = parseJSON(lvl.requirements) ?? {}; - const rewards = parseJSON(lvl.rewards) ?? { table: [] }; - const mobs = parseJSON(lvl.mobs) ?? { table: [] }; - - // Validar herramienta si aplica - const reqRes = await validateRequirements( - userId, - guildId, - requirements, - opts?.toolKey - ); - - // Aplicar recompensas y samplear mobs - const { rewards: delivered, modifiers: rewardModifiers } = await applyRewards( - userId, - guildId, - rewards - ); - const mobsSpawned = await sampleMobInstances(mobs, level); - - // container visible for the whole runMinigame scope so we can attach mob-derived modifiers - let mergedRewardModifiers: RunResult["rewardModifiers"] | undefined = - undefined; - - // --- Aplicar rewardMods de los mobs (coinMultiplier y extraDropChance) - // Nota: applyRewards ya aplicó monedas/items base. Aquí solo aplicamos - // incrementos por rewardMods de mobs: aumentos positivos en monedas y - // posibles drops extra (aquí simplificado como +1 moneda por evento). - try { - // calcular total de monedas entregadas hasta ahora - const totalCoins = delivered - .filter((r) => r.type === "coins") - .reduce((s, r) => s + (r.amount || 0), 0); - - // multiplicador compuesto por mobs (producto de coinMultiplier) - const mobCoinMultiplier = mobsSpawned.reduce((acc, m) => { - const cm = (m && (m.rewardMods as any)?.coinMultiplier) ?? 1; - return acc * (typeof cm === "number" ? cm : 1); - }, 1); - - // Si el multiplicador es mayor a 1, añadimos la diferencia en monedas - if (mobCoinMultiplier > 1 && totalCoins > 0) { - const newTotal = Math.max(0, Math.floor(totalCoins * mobCoinMultiplier)); - const delta = newTotal - totalCoins; - if (delta !== 0) { - await adjustCoins(userId, guildId, delta); - delivered.push({ type: "coins", amount: delta }); - } - } - - // extraDropChance: por cada mob, tirada para dar un drop. - // Si la definición del mob incluye `drops` (tabla de items), intentamos elegir uno. - // Si no hay drops configurados o la selección falla, otorgamos 1 coin como fallback. - let extraDropsGiven = 0; - for (const m of mobsSpawned) { - const chance = (m && (m.rewardMods as any)?.extraDropChance) ?? 0; - if (typeof chance === "number" && chance > 0 && Math.random() < chance) { - try { - // Intentar usar la tabla `drops` si existe en la definición original (buscada via findMobDef) - const def = (m && findMobDef(m.key)) as any; - const drops = def?.drops ?? def?.rewards ?? null; - let granted = false; - if (drops) { - // Formato A (ponderado): [{ itemKey, qty?, weight? }, ...] - if (Array.isArray(drops) && drops.length > 0) { - const total = drops.reduce( - (s: number, d: any) => s + (Number(d.weight) || 1), - 0 - ); - let r = Math.random() * total; - for (const d of drops) { - const w = Number(d.weight) || 1; - r -= w; - if (r <= 0) { - const sel = d.itemKey; - const qty = Number(d.qty) || 1; - await addItemByKey(userId, guildId, sel, qty); - delivered.push({ type: "item", itemKey: sel, qty }); - granted = true; - break; - } - } - } else if (typeof drops === "object") { - // Formato B (map simple): { itemKey: qty } - const keys = Object.keys(drops || {}); - if (keys.length > 0) { - const sel = keys[Math.floor(Math.random() * keys.length)]; - const qty = Number((drops as any)[sel]) || 1; - await addItemByKey(userId, guildId, sel, qty); - delivered.push({ type: "item", itemKey: sel, qty }); - granted = true; - } - } - } - if (!granted) { - // fallback: coin - await adjustCoins(userId, guildId, 1); - delivered.push({ type: "coins", amount: 1 }); - } - extraDropsGiven++; - } catch (e) { - // en error, conceder fallback monetario pero no interrumpir - try { - await adjustCoins(userId, guildId, 1); - delivered.push({ type: "coins", amount: 1 }); - extraDropsGiven++; - } catch {} - } - } - } - - // Construir un objeto mergedRewardModifiers en lugar de mutar rewardModifiers - mergedRewardModifiers = { - ...((rewardModifiers as any) || {}), - mobCoinMultiplier, - extraDropsGiven: - ((rewardModifiers as any)?.extraDropsGiven || 0) + extraDropsGiven, - } as any; - } catch (err) { - // No queremos que fallos menores de rewardMods rompan la ejecución - // eslint-disable-next-line no-console - console.warn("applyMobRewardMods: failed:", (err as any)?.message ?? err); - } - - // Reducir durabilidad de herramienta si se usó - let toolInfo: RunResult["tool"] | undefined; - if (reqRes.toolKeyUsed) { - const t = await reduceToolDurability(userId, guildId, reqRes.toolKeyUsed); - toolInfo = { - key: reqRes.toolKeyUsed, - durabilityDelta: t.delta, - broken: t.broken, - remaining: t.remaining, - max: t.max, - brokenInstance: t.brokenInstance, - instancesRemaining: t.instancesRemaining, - toolSource: reqRes.toolSource ?? (opts?.toolKey ? "provided" : "auto"), - }; - } - - // (Eliminado combate placeholder; sustituido por sistema integrado más abajo) - // --- Combate Integrado con Equipo y HP Persistente --- - let combatSummary: CombatSummary | undefined; - if (mobsSpawned.length > 0) { - // Obtener stats efectivos del jugador (arma = daño, armadura = defensa, capa = maxHp extra + mutaciones) - const eff = await getEffectiveStats(userId, guildId); - const playerState = await ensurePlayerState(userId, guildId); - const startHp = eff.hp; // HP actual persistente - - // ⚠️ CRÍTICO: Validar que el jugador tenga arma equipada ANTES de iniciar combate - // Regla: si el jugador no tiene arma (damage <=0) no puede infligir daño real y perderá automáticamente. - const hasWeapon = eff.damage > 0; - - if (!hasWeapon) { - // Registrar derrota simple contra la lista de mobs (no se derrotan mobs). - const mobLogs: CombatSummary["mobs"] = mobsSpawned.map((m) => ({ - mobKey: m?.key ?? "unknown", - maxHp: 0, - defeated: false, - totalDamageDealt: 0, - totalDamageTakenFromMob: 0, - rounds: [], - })); - // Aplicar daño simulado: mobs atacan una vez (opcional). Aquí asumimos que el jugador cae a 0 directamente para simplificar. - const endHp = Math.max(1, Math.floor(eff.maxHp * 0.5)); - await adjustHP(userId, guildId, endHp - playerState.hp); // regen al 50% - await updateStats(userId, guildId, { - damageTaken: 0, // opcional: podría ponerse un valor fijo si quieres penalizar - timesDefeated: 1, - } as any); - // Reset de racha si existía - await prisma.playerStats.update({ - where: { userId_guildId: { userId, guildId } }, - data: { currentWinStreak: 0 }, - }); - // Penalizaciones por derrota: pérdida de oro + fatiga - let deathPenalty: CombatSummary["deathPenalty"] | undefined; - try { - const wallet = await getOrCreateWallet(userId, guildId); - const coins = wallet.coins; - const percent = computeDeathPenaltyPercent(area, level); - let goldLost = 0; - if (coins > 0) { - goldLost = Math.floor(coins * percent); - if (goldLost < 1) goldLost = 1; - if (goldLost > 5000) goldLost = 5000; // nuevo cap más alto por riesgo escalado - if (goldLost > coins) goldLost = coins; // no perder más de lo que tienes - if (goldLost > 0) { - await prisma.economyWallet.update({ - where: { userId_guildId: { userId, guildId } }, - data: { coins: { decrement: goldLost } }, - }); - } - } - // Fatiga escalada: base 15%, +1% cada 5 de racha previa (cap +10%) - let previousStreak = 0; - try { - const ps = await prisma.playerStats.findUnique({ - where: { userId_guildId: { userId, guildId } }, - }); - previousStreak = ps?.currentWinStreak || 0; - } catch {} - const extraFatigue = Math.min( - 0.1, - Math.floor(previousStreak / 5) * 0.01 - ); - const fatigueMagnitude = 0.15 + extraFatigue; - const fatigueMinutes = 5; - await applyDeathFatigue( - userId, - guildId, - fatigueMagnitude, - fatigueMinutes - ); - deathPenalty = { - goldLost, - fatigueAppliedMinutes: fatigueMinutes, - fatigueMagnitude, - percentApplied: percent, - }; - try { - await prisma.deathLog.create({ - data: { - userId, - guildId, - areaId: area.id, - areaKey: area.key, - level, - goldLost: goldLost || 0, - percentApplied: percent, - autoDefeatNoWeapon: true, - fatigueMagnitude, - fatigueMinutes, - metadata: {}, - }, - }); - } catch {} - combatSummary = { - mobs: mobLogs, - totalDamageDealt: 0, - totalDamageTaken: 0, - mobsDefeated: 0, - victory: false, - playerStartHp: startHp, - playerEndHp: endHp, - outcome: "defeat", - autoDefeatNoWeapon: true, - deathPenalty, - }; - } catch { - combatSummary = { - mobs: mobLogs, - totalDamageDealt: 0, - totalDamageTaken: 0, - mobsDefeated: 0, - victory: false, - playerStartHp: startHp, - playerEndHp: endHp, - outcome: "defeat", - autoDefeatNoWeapon: true, - }; - } - } else { - let currentHp = startHp; - const mobLogs: CombatSummary["mobs"] = []; - let totalDealt = 0; - let totalTaken = 0; - let totalMobsDefeated = 0; - // Variación de ±20% - const variance = (base: number) => { - const factor = 0.8 + Math.random() * 0.4; // 0.8 - 1.2 - return base * factor; - }; - for (const mob of mobsSpawned) { - if (currentHp <= 0) break; // jugador derrotado antes de iniciar este mob - // Stats simples del mob (usamos la instancia escalada) - const mobBaseHp = Math.max(1, Math.floor(mob?.scaled?.hp ?? 10)); - let mobHp = mobBaseHp; - const rounds: any[] = []; - let round = 1; - let mobDamageDealt = 0; // daño que jugador hace a este mob - let mobDamageTakenFromMob = 0; // daño que jugador recibe de este mob - while ( - mobHp > 0 && - currentHp > 0 && - round <= (mob?.behavior?.maxRounds ?? 12) - ) { - // Daño jugador -> mob - const playerRaw = variance(eff.damage || 1) + 1; // asegurar >=1 - const playerDamage = Math.max(1, Math.round(playerRaw)); - mobHp -= playerDamage; - mobDamageDealt += playerDamage; - totalDealt += playerDamage; - let playerTaken = 0; - if (mobHp > 0) { - const mobAtkBase = 3 + Math.random() * 4; // 3-7 - const mobAtk = variance(mobAtkBase); - // Mitigación por defensa => defensa reduce linealmente hasta 60% cap - const mitigationRatio = Math.min(0.6, (eff.defense || 0) * 0.05); // 5% por punto defensa hasta 60% - const mitigated = mobAtk * (1 - mitigationRatio); - playerTaken = Math.max(0, Math.round(mitigated)); - if (playerTaken > 0) { - currentHp = Math.max(0, currentHp - playerTaken); - mobDamageTakenFromMob += playerTaken; - totalTaken += playerTaken; - } - } - rounds.push({ - mobKey: mob?.key ?? "unknown", - round, - playerDamageDealt: playerDamage, - playerDamageTaken: playerTaken, - mobRemainingHp: Math.max(0, mobHp), - mobDefeated: mobHp <= 0, - }); - if (mobHp <= 0) { - totalMobsDefeated++; - break; - } - if (currentHp <= 0) break; - round++; - } - mobLogs.push({ - mobKey: mob?.key ?? "unknown", - maxHp: mobBaseHp, - defeated: mobHp <= 0, - totalDamageDealt: mobDamageDealt, - totalDamageTakenFromMob: mobDamageTakenFromMob, - rounds, - }); - if (currentHp <= 0) break; // fin combate global - } - const victory = currentHp > 0 && totalMobsDefeated === mobsSpawned.length; - // Persistir HP (si derrota -> regenerar al 50% del maxHp, regla confirmada por usuario) - let endHp = currentHp; - let defeatedNow = false; - if (currentHp <= 0) { - defeatedNow = true; - const regen = Math.max(1, Math.floor(eff.maxHp * 0.5)); - endHp = regen; - await adjustHP(userId, guildId, regen - playerState.hp); // set a 50% (delta relativo) - } else { - // almacenar HP restante real - await adjustHP(userId, guildId, currentHp - playerState.hp); - } - // Actualizar estadísticas - const statUpdates: Record = {}; - if (area.key.startsWith("mine")) statUpdates.minesCompleted = 1; - if (area.key.startsWith("lagoon")) statUpdates.fishingCompleted = 1; - if ( - area.key.startsWith("arena") || - area.key.startsWith("battle") || - area.key.includes("fight") - ) - statUpdates.fightsCompleted = 1; - if (totalMobsDefeated > 0) statUpdates.mobsDefeated = totalMobsDefeated; - if (totalDealt > 0) statUpdates.damageDealt = totalDealt; - if (totalTaken > 0) statUpdates.damageTaken = totalTaken; - if (defeatedNow) statUpdates.timesDefeated = 1; - // Rachas de victoria - if (victory) { - statUpdates.currentWinStreak = 1; // increment - } else if (defeatedNow) { - // reset current streak - // No podemos hacer decrement directo, así que setearemos manual luego - } - await updateStats(userId, guildId, statUpdates as any); - if (defeatedNow) { - await prisma.playerStats.update({ - where: { userId_guildId: { userId, guildId } }, - data: { currentWinStreak: 0 }, - }); - // Penalizaciones por derrota - let deathPenalty: CombatSummary["deathPenalty"] | undefined; - try { - const wallet = await getOrCreateWallet(userId, guildId); - const coins = wallet.coins; - const percent = computeDeathPenaltyPercent(area, level); - let goldLost = 0; - if (coins > 0) { - goldLost = Math.floor(coins * percent); - if (goldLost < 1) goldLost = 1; - if (goldLost > 5000) goldLost = 5000; - if (goldLost > coins) goldLost = coins; - if (goldLost > 0) { - await prisma.economyWallet.update({ - where: { userId_guildId: { userId, guildId } }, - data: { coins: { decrement: goldLost } }, - }); - } - } - // Fatiga escalada - let previousStreak = 0; - try { - const ps = await prisma.playerStats.findUnique({ - where: { userId_guildId: { userId, guildId } }, - }); - previousStreak = ps?.currentWinStreak || 0; - } catch {} - const extraFatigue = Math.min( - 0.1, - Math.floor(previousStreak / 5) * 0.01 - ); - const fatigueMagnitude = 0.15 + extraFatigue; - const fatigueMinutes = 5; - await applyDeathFatigue( - userId, - guildId, - fatigueMagnitude, - fatigueMinutes - ); - deathPenalty = { - goldLost, - fatigueAppliedMinutes: fatigueMinutes, - fatigueMagnitude, - percentApplied: percent, - }; - try { - await prisma.deathLog.create({ - data: { - userId, - guildId, - areaId: area.id, - areaKey: area.key, - level, - goldLost: goldLost || 0, - percentApplied: percent, - autoDefeatNoWeapon: false, - fatigueMagnitude, - fatigueMinutes, - metadata: { mobs: totalMobsDefeated }, - }, - }); - } catch {} - } catch { - // silencioso - } - combatSummary = { - mobs: mobLogs, - totalDamageDealt: totalDealt, - totalDamageTaken: totalTaken, - mobsDefeated: totalMobsDefeated, - victory, - playerStartHp: startHp, - playerEndHp: endHp, - outcome: "defeat", - deathPenalty, - }; - } else { - if (victory) { - await prisma.$executeRawUnsafe( - `UPDATE "PlayerStats" SET "longestWinStreak" = GREATEST("longestWinStreak", "currentWinStreak") WHERE "userId" = $1 AND "guildId" = $2`, - userId, - guildId - ); - } - combatSummary = { - mobs: mobLogs, - totalDamageDealt: totalDealt, - totalDamageTaken: totalTaken, - mobsDefeated: totalMobsDefeated, - victory, - playerStartHp: startHp, - playerEndHp: endHp, - outcome: victory ? "victory" : "defeat", - }; - } - } - } - - // Registrar la ejecución - let weaponToolInfo: RunResult["weaponTool"] | undefined; - // Si hubo combate y el jugador tenía un arma equipada distinta de la herramienta de recolección, degradarla. - if (combatSummary && combatSummary.mobs.length > 0) { - try { - const { weapon } = await getEquipment(userId, guildId); - if (weapon) { - const weaponProps = parseItemProps(weapon.props); - if (weaponProps?.tool?.type === "sword") { - // Evitar degradar dos veces si la herramienta principal ya era la espada usada para recoger (no aplica en mina/pesca normalmente) - const alreadyMain = toolInfo?.key === weapon.key; - if (!alreadyMain) { - const wt = await reduceToolDurability( - userId, - guildId, - weapon.key, - "combat" - ); - weaponToolInfo = { - key: weapon.key, - durabilityDelta: wt.delta, - broken: wt.broken, - remaining: wt.remaining, - max: wt.max, - brokenInstance: wt.brokenInstance, - instancesRemaining: wt.instancesRemaining, - toolSource: "equipped", - }; - } else { - // Si la espada era también la herramienta (pelear) ya se degradó en la fase de tool principal - weaponToolInfo = undefined; - } - } - } - } catch { - // silencioso - } - } - - // --- Aplicar rewardMods provenientes de mobs derrotados (post-combate) - try { - if (combatSummary) { - const defeated = (combatSummary.mobs || []).filter((m) => m.defeated); - if (defeated.length > 0) { - // multiplicador compuesto solo por mobs derrotados - const defeatedMultiplier = defeated.reduce((acc, dm) => { - try { - const def = findMobDef(dm.mobKey) as any; - const cm = - (def && def.rewardMods && def.rewardMods.coinMultiplier) ?? 1; - return acc * (typeof cm === "number" ? cm : 1); - } catch { - return acc; - } - }, 1); - - const totalCoinsBefore = delivered - .filter((r) => r.type === "coins") - .reduce((s, r) => s + (r.amount || 0), 0); - - if (defeatedMultiplier > 1 && totalCoinsBefore > 0) { - const newTotal = Math.max( - 0, - Math.floor(totalCoinsBefore * defeatedMultiplier) - ); - const delta = newTotal - totalCoinsBefore; - if (delta !== 0) { - await adjustCoins(userId, guildId, delta); - delivered.push({ type: "coins", amount: delta }); - } - } - - // extra drops por cada mob derrotado - let extraDropsFromCombat = 0; - for (const dm of defeated) { - try { - const def = findMobDef(dm.mobKey) as any; - const chance = - (def && def.rewardMods && def.rewardMods.extraDropChance) ?? 0; - if ( - typeof chance === "number" && - chance > 0 && - Math.random() < chance - ) { - // intentar dropear item similar al flujo de minigame - const drops = def?.drops ?? def?.rewards ?? null; - let granted = false; - if (drops) { - if (Array.isArray(drops) && drops.length > 0) { - const total = drops.reduce( - (s: number, d: any) => s + (Number(d.weight) || 1), - 0 - ); - let r = Math.random() * total; - for (const d of drops) { - const w = Number(d.weight) || 1; - r -= w; - if (r <= 0) { - const sel = d.itemKey; - const qty = Number(d.qty) || 1; - await addItemByKey(userId, guildId, sel, qty); - delivered.push({ type: "item", itemKey: sel, qty }); - granted = true; - break; - } - } - } else if (typeof drops === "object") { - const keys = Object.keys(drops || {}); - if (keys.length > 0) { - const sel = keys[Math.floor(Math.random() * keys.length)]; - const qty = Number((drops as any)[sel]) || 1; - await addItemByKey(userId, guildId, sel, qty); - delivered.push({ type: "item", itemKey: sel, qty }); - granted = true; - } - } - } - if (!granted) { - await adjustCoins(userId, guildId, 1); - delivered.push({ type: "coins", amount: 1 }); - } - extraDropsFromCombat++; - } - } catch {} - } - - // Fusionar con mergedRewardModifiers - mergedRewardModifiers = { - ...((mergedRewardModifiers as any) || (rewardModifiers as any) || {}), - defeatedMobCoinMultiplier: - ((mergedRewardModifiers as any)?.defeatedMobCoinMultiplier || 1) * - (defeatedMultiplier || 1), - extraDropsFromCombat: - ((mergedRewardModifiers as any)?.extraDropsFromCombat || 0) + - extraDropsFromCombat, - } as any; - } - } - } catch (e) { - // no bloquear ejecución por fallos en recompensas secundarias - } - - const resultJson: Prisma.InputJsonValue = { - rewards: delivered, - mobs: mobsSpawned.map((m) => m?.key ?? "unknown"), - tool: toolInfo, - weaponTool: weaponToolInfo, - combat: combatSummary, - rewardModifiers: - mergedRewardModifiers ?? (rewardModifiers as any) ?? undefined, - notes: "auto", - } as unknown as Prisma.InputJsonValue; - - await prisma.minigameRun.create({ - data: { - userId, - guildId, - areaId: area.id, - level, - toolItemId: null, // opcional si decides guardar id del item herramienta - success: true, - result: resultJson, - }, - }); - - // Progreso del jugador - await prisma.playerProgress.upsert({ - where: { userId_guildId_areaId: { userId, guildId, areaId: area.id } }, - create: { - userId, - guildId, - areaId: area.id, - highestLevel: Math.max(1, level), - }, - update: { highestLevel: { set: level } }, - }); - - // Setear cooldown - if (cdSeconds > 0) { - await prisma.actionCooldown.upsert({ - where: { userId_guildId_key: { userId, guildId, key: cdKey } }, - update: { until: new Date(Date.now() + cdSeconds * 1000) }, - create: { - userId, - guildId, - key: cdKey, - until: new Date(Date.now() + cdSeconds * 1000), - }, - }); - } - - return { - success: true, - rewards: delivered, - mobs: mobsSpawned.map((m) => m?.key ?? "unknown"), - tool: toolInfo, - weaponTool: weaponToolInfo, - combat: combatSummary, - rewardModifiers: mergedRewardModifiers, - }; -} - -// Convenience wrappers with auto-level (from PlayerProgress) and auto-tool selection inside validateRequirements -export async function runMining( - userId: string, - guildId: string, - level?: number, - toolKey?: string -) { - const area = await prisma.gameArea.findFirst({ - where: { key: "mine.cavern", OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); - if (!area) throw new Error("Área de mina no configurada"); - const lvl = - level ?? - ( - await prisma.playerProgress.findUnique({ - where: { userId_guildId_areaId: { userId, guildId, areaId: area.id } }, - }) - )?.highestLevel ?? - 1; - return runMinigame(userId, guildId, "mine.cavern", Math.max(1, lvl), { - toolKey, - }); -} - -export async function runFishing( - userId: string, - guildId: string, - level?: number, - toolKey?: string -) { - const area = await prisma.gameArea.findFirst({ - where: { key: "lagoon.shore", OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); - if (!area) throw new Error("Área de laguna no configurada"); - const lvl = - level ?? - ( - await prisma.playerProgress.findUnique({ - where: { userId_guildId_areaId: { userId, guildId, areaId: area.id } }, - }) - )?.highestLevel ?? - 1; - return runMinigame(userId, guildId, "lagoon.shore", Math.max(1, lvl), { - toolKey, - }); -} diff --git a/src/.backup/game_core/game/minigames/testHelpers.ts b/src/.backup/game_core/game/minigames/testHelpers.ts deleted file mode 100644 index 714a1e6..0000000 --- a/src/.backup/game_core/game/minigames/testHelpers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { findMobDef } from "../mobs/mobData"; - -export function pickDropFromDef( - def: any -): { itemKey: string; qty: number } | null { - if (!def) return null; - const drops = def.drops ?? def.rewards ?? null; - if (!drops) return null; - if (Array.isArray(drops) && drops.length > 0) { - const total = drops.reduce( - (s: number, d: any) => s + (Number(d.weight) || 1), - 0 - ); - let r = Math.random() * total; - for (const d of drops) { - const w = Number(d.weight) || 1; - r -= w; - if (r <= 0) return { itemKey: d.itemKey, qty: Number(d.qty) || 1 }; - } - return { - itemKey: drops[drops.length - 1].itemKey, - qty: Number(drops[drops.length - 1].qty) || 1, - }; - } - if (typeof drops === "object") { - const keys = Object.keys(drops || {}); - if (keys.length === 0) return null; - const sel = keys[Math.floor(Math.random() * keys.length)]; - return { itemKey: sel, qty: Number(drops[sel]) || 1 }; - } - return null; -} diff --git a/src/.backup/game_core/game/minigames/types.ts b/src/.backup/game_core/game/minigames/types.ts deleted file mode 100644 index a81086b..0000000 --- a/src/.backup/game_core/game/minigames/types.ts +++ /dev/null @@ -1,120 +0,0 @@ -// Tipos para motor de minijuegos (mina, pesca, pelea, plantar). -// JSON flexible: estos tipos solo guían el shape esperado en GameAreaLevel.requirements/rewards/mobs. - -export type ToolRequirement = { - required?: boolean; // si se requiere herramienta - toolType?: string; // 'pickaxe' | 'rod' | 'sword' | ... - minTier?: number; // nivel mínimo de herramienta - allowedKeys?: string[]; // lista blanca de item keys específicos -}; - -export type LevelRequirements = { - tool?: ToolRequirement; - // extensible: stamina, consumibles, roles requeridos, etc. - [k: string]: unknown; -}; - -export type WeightedReward = - | { type: "coins"; amount: number; weight: number } - | { type: "item"; itemKey: string; qty: number; weight: number }; - -export type RewardsTable = { - draws?: number; // cuántas extracciones realizar (default 1) - table: WeightedReward[]; -}; - -export type WeightedMob = { mobKey: string; weight: number }; - -export type MobsTable = { - draws?: number; // cuántos mobs intentar spawnear (default 0) - table: WeightedMob[]; -}; - -export type LevelConfig = { - requirements?: LevelRequirements; - rewards?: RewardsTable; - mobs?: MobsTable; - // extensible: multiplicadores, riesgos, etc. - [k: string]: unknown; -}; - -export type RunMinigameOptions = { - toolKey?: string; // herramienta elegida por el jugador -}; - -export type RunResult = { - success: boolean; - rewards: Array<{ - type: "coins" | "item"; - amount?: number; - itemKey?: string; - qty?: number; - }>; - mobs: string[]; // keys de mobs spawneados - tool?: { - key?: string; - durabilityDelta?: number; // cuanto se redujo en esta ejecución - broken?: boolean; // si se rompió en este uso - remaining?: number; // durabilidad restante después de aplicar delta (si aplica) - max?: number; // durabilidad máxima configurada - brokenInstance?: boolean; // true si solo se rompió una instancia - instancesRemaining?: number; // instancias que quedan después del uso - toolSource?: "provided" | "equipped" | "auto"; // origen de la selección - }; - // Nueva: arma usada en combate (se degrada con un multiplicador menor para evitar roturas instantáneas) - weaponTool?: { - key?: string; - durabilityDelta?: number; - broken?: boolean; - remaining?: number; - max?: number; - brokenInstance?: boolean; - instancesRemaining?: number; - toolSource?: "equipped"; // siempre proviene del slot de arma - }; - combat?: CombatSummary; // resumen de combate si hubo mobs y se procesó - // Modificadores aplicados a las recompensas (ej: penalización por FATIGUE sobre monedas) - rewardModifiers?: { - fatigueCoinMultiplier?: number; // 0.85 si hay -15% - fatigueMagnitude?: number; // magnitud original del efecto - baseCoinsAwarded?: number; // suma antes de aplicar multiplicador de fatiga - coinsAfterPenalty?: number; // suma final depositada en wallet - }; -}; - -// --- Combate Básico --- -export type CombatRound = { - mobKey: string; - round: number; - playerDamageDealt: number; // daño infligido al mob en esta ronda - playerDamageTaken: number; // daño recibido del mob en esta ronda - mobRemainingHp: number; // hp restante del mob tras la ronda - mobDefeated?: boolean; -}; - -export type CombatMobLog = { - mobKey: string; - maxHp: number; - defeated: boolean; - totalDamageDealt: number; - totalDamageTakenFromMob: number; // daño que el jugador recibió de este mob - rounds: CombatRound[]; -}; - -export type CombatSummary = { - mobs: CombatMobLog[]; - totalDamageDealt: number; - totalDamageTaken: number; - mobsDefeated: number; - victory: boolean; // true si el jugador sobrevivió a todos los mobs - playerStartHp?: number; - playerEndHp?: number; - outcome?: "victory" | "defeat"; - autoDefeatNoWeapon?: boolean; // true si la derrota fue inmediata por no tener arma (damage <= 0) - deathPenalty?: { - goldLost?: number; - fatigueAppliedMinutes?: number; - fatigueMagnitude?: number; // 0.15 = 15% - percentApplied?: number; // porcentaje calculado dinámicamente según área/nivel - }; -}; diff --git a/src/.backup/game_core/game/mobs/README.md b/src/.backup/game_core/game/mobs/README.md deleted file mode 100644 index e25f9f4..0000000 --- a/src/.backup/game_core/game/mobs/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Mobs module - -## Propósito - -- Contener definiciones de mobs (plantillas) y helpers para obtener instancias escaladas por nivel. - -## Convenciones - -- `MOB_DEFINITIONS` contiene objetos `BaseMobDefinition` con configuración declarativa. -- Usar `getMobInstance(key, areaLevel)` para obtener una instancia lista para combate. -- Evitar lógica de combate en este archivo; este módulo solo expone datos y transformaciones determinísticas. - -## Futuro - -- Migrar `MOB_DEFINITIONS` a la base de datos o AppWrite y añadir cache si se requiere edición en runtime. -- Añadir validadores y tests para las definiciones. diff --git a/src/.backup/game_core/game/mobs/admin.ts b/src/.backup/game_core/game/mobs/admin.ts deleted file mode 100644 index 8ddb9e0..0000000 --- a/src/.backup/game_core/game/mobs/admin.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { prisma } from "../../core/database/prisma"; -import { z } from "zod"; -import { - BaseMobDefinition, - MOB_DEFINITIONS, - findMobDef, - BaseMobDefinitionSchema, -} from "./mobData"; - -type MobInput = z.infer; - -export type CreateOrUpdateResult = { - def: BaseMobDefinition; - row?: any; -}; - -function prismaMobAvailable(): boolean { - const anyPrisma: any = prisma as any; - if (!process.env.XATA_DB) return false; - return !!( - anyPrisma && - anyPrisma.mob && - typeof anyPrisma.mob.create === "function" - ); -} - -export async function listMobs(): Promise { - const rows = await listMobsWithRows(); - return rows.map((r) => r.def); -} - -export type MobWithRow = { - def: BaseMobDefinition; - id?: string | null; - guildId?: string | null; - isDb?: boolean; -}; - -export async function listMobsWithRows(): Promise { - const map: Record = {}; - // Start with built-ins - for (const d of MOB_DEFINITIONS) { - map[d.key] = { def: d, id: null, guildId: null, isDb: false }; - } - - if (!prismaMobAvailable()) { - return Object.values(map); - } - - try { - const anyPrisma: any = prisma as any; - const rows = await anyPrisma.mob.findMany(); - // eslint-disable-next-line no-console - console.info(`listMobsWithRows: DB returned ${rows.length} rows`); - for (const r of rows) { - const cfg = - r.metadata ?? - r.stats ?? - r.drops ?? - r.config ?? - r.definition ?? - r.data ?? - null; - if (!cfg || typeof cfg !== "object") continue; - try { - const parsed = BaseMobDefinitionSchema.parse(cfg as any); - map[parsed.key] = { - def: parsed, - id: r.id ?? null, - guildId: r.guildId ?? null, - isDb: true, - }; - } catch (e) { - // eslint-disable-next-line no-console - console.warn( - "Skipping invalid mob row id=", - r.id, - (e as any)?.errors ?? e - ); - } - } - } catch (e) { - // eslint-disable-next-line no-console - console.warn("listMobsWithRows: DB read failed:", (e as any)?.message ?? e); - } - - return Object.values(map).sort((a, b) => a.def.key.localeCompare(b.def.key)); -} - -export async function getMob(key: string): Promise { - // Check DB first - if (prismaMobAvailable()) { - try { - const anyPrisma: any = prisma as any; - const row = await anyPrisma.mob.findFirst({ where: { key } }); - if (row) { - const cfg = - row.metadata ?? - row.stats ?? - row.drops ?? - row.config ?? - row.definition ?? - row.data ?? - null; - if (cfg) { - try { - return BaseMobDefinitionSchema.parse(cfg as any); - } catch (e) { - return null; - } - } - } - } catch (e) { - // ignore DB issues - } - } - // Fallback to built-ins - return findMobDef(key); -} - -export async function createOrUpdateMob( - input: MobInput & { guildId?: string; category?: string } -): Promise { - const parsed = BaseMobDefinitionSchema.parse(input); - let row: any | undefined; - if (prismaMobAvailable()) { - try { - const anyPrisma: any = prisma as any; - const where: any = { key: parsed.key }; - if (input.guildId) where.guildId = input.guildId; - const existing = await anyPrisma.mob.findFirst({ where }); - if (existing) { - row = await anyPrisma.mob.update({ - where: { id: existing.id }, - data: { - name: parsed.name, - category: (input as any).category ?? null, - metadata: parsed, - }, - }); - // eslint-disable-next-line no-console - console.info( - `createOrUpdateMob: updated mob id=${row.id} key=${parsed.key}` - ); - } else { - row = await anyPrisma.mob.create({ - data: { - key: parsed.key, - name: parsed.name, - category: (input as any).category ?? null, - guildId: input.guildId ?? null, - metadata: parsed, - }, - }); - // eslint-disable-next-line no-console - console.info( - `createOrUpdateMob: created mob id=${row.id} key=${parsed.key}` - ); - } - } catch (e) { - // if DB fails, fallthrough to return parsed but do not throw - // eslint-disable-next-line no-console - console.warn( - "createOrUpdateMob: DB save failed:", - (e as any)?.message ?? e - ); - } - } - return { def: parsed, row }; -} - -export async function deleteMob(key: string): Promise { - if (prismaMobAvailable()) { - try { - const anyPrisma: any = prisma as any; - const existing = await anyPrisma.mob.findFirst({ where: { key } }); - if (existing) { - await anyPrisma.mob.delete({ where: { id: existing.id } }); - // eslint-disable-next-line no-console - console.info(`deleteMob: deleted mob id=${existing.id} key=${key}`); - return true; - } - } catch (e) { - // ignore - // eslint-disable-next-line no-console - console.warn("deleteMob: DB delete failed:", (e as any)?.message ?? e); - return false; - } - } - // If no DB or not found, attempt to delete from in-memory builtins (no-op) - return false; -} - -export async function ensureMobRepoUpToDate() { - // helper to tell mobData to refresh caches — import dynamically to avoid cycles - try { - const mod = await import("./mobData.js"); - if (typeof mod.refreshMobDefinitionsFromDb === "function") { - await mod.refreshMobDefinitionsFromDb(); - } - if (typeof mod.validateAllMobDefs === "function") { - mod.validateAllMobDefs(); - } - } catch (e) { - // ignore - } -} diff --git a/src/.backup/game_core/game/mobs/mobData.ts b/src/.backup/game_core/game/mobs/mobData.ts deleted file mode 100644 index 547ac8e..0000000 --- a/src/.backup/game_core/game/mobs/mobData.ts +++ /dev/null @@ -1,282 +0,0 @@ -// Definición declarativa de mobs (scaffolding) -// Futuro: migrar a tabla prisma.mob enriquecida o cache Appwrite. - -export interface BaseMobDefinition { - key: string; // identificador único - name: string; // nombre visible - tier: number; // escala de dificultad base - base: { - hp: number; - attack: number; - defense?: number; - }; - scaling?: { - hpPerLevel?: number; // incremento por nivel de área - attackPerLevel?: number; - defensePerLevel?: number; - hpMultiplierPerTier?: number; // multiplicador adicional por tier - }; - tags?: string[]; // p.ej. ['undead','beast'] - rewardMods?: { - coinMultiplier?: number; - extraDropChance?: number; // 0-1 - }; - behavior?: { - maxRounds?: number; // override límite de rondas - aggressive?: boolean; // si ataca siempre - critChance?: number; // 0-1 - critMultiplier?: number; // default 1.5 - }; -} - -// Nota sobre 'drops': -// El motor soporta, además de la definición básica del mob, un campo opcional `drops` -// que puede vivir en la definición del mob (o en la fila DB `Mob.drops`). Hay dos formatos -// soportados por conveniencia: -// 1) Mapa simple (object): { "item.key": qty, "other.key": qty } -// - Selección aleatoria entre las keys y entrega qty del item seleccionado. -// 2) Array ponderado: [{ itemKey: string, qty?: number, weight?: number }, ...] -// - Se realiza una tirada ponderada usando `weight` (por defecto 1) y se entrega `qty`. -// Si no hay drops configurados o la selección falla, la lógica actual aplica un fallback que -// otorga 1 moneda. - -// Ejemplos iniciales - se pueden ir expandiendo -export const MOB_DEFINITIONS: BaseMobDefinition[] = [ - { - key: "slime.green", - name: "Slime Verde", - tier: 1, - base: { hp: 18, attack: 4 }, - scaling: { hpPerLevel: 3, attackPerLevel: 0.5 }, - tags: ["slime"], - rewardMods: { coinMultiplier: 0.9 }, - behavior: { maxRounds: 12, aggressive: true }, - }, - { - key: "skeleton.basic", - name: "Esqueleto", - tier: 2, - base: { hp: 30, attack: 6, defense: 1 }, - scaling: { hpPerLevel: 4, attackPerLevel: 0.8, defensePerLevel: 0.2 }, - tags: ["undead"], - rewardMods: { coinMultiplier: 1.1, extraDropChance: 0.05 }, - behavior: { aggressive: true, critChance: 0.05, critMultiplier: 1.5 }, - }, -]; - -export function computeMobStats(def: BaseMobDefinition, areaLevel: number) { - const lvl = Math.max(1, areaLevel); - const s = def.scaling || {}; - const hp = Math.round(def.base.hp + (s.hpPerLevel ?? 0) * (lvl - 1)); - const atk = +(def.base.attack + (s.attackPerLevel ?? 0) * (lvl - 1)).toFixed( - 2 - ); - const defVal = +( - (def.base.defense ?? 0) + - (s.defensePerLevel ?? 0) * (lvl - 1) - ).toFixed(2); - return { hp, attack: atk, defense: defVal }; -} - -/** - * MobInstance: representación de una entidad mob lista para usarse en combate. - * - incluye stats escaladas por nivel de área (hp, attack, defense) - * - preserva la definición base para referencias (name, tier, tags, behavior) - */ -export interface MobInstance { - key: string; - name: string; - tier: number; - base: BaseMobDefinition["base"]; - scaled: { hp: number; attack: number; defense: number }; - tags?: string[]; - rewardMods?: BaseMobDefinition["rewardMods"]; - behavior?: BaseMobDefinition["behavior"]; -} - -/** - * getMobInstance: devuelve una instancia de mob con stats calculadas. - * Si la definición no existe, devuelve null. - */ -export function getMobInstance( - key: string, - areaLevel: number -): MobInstance | null { - const def = findMobDef(key); - if (!def) return null; - const scaled = computeMobStats(def, areaLevel); - return { - key: def.key, - name: def.name, - tier: def.tier, - base: def.base, - scaled, - tags: def.tags, - rewardMods: def.rewardMods, - behavior: def.behavior, - }; -} - -export function listMobKeys(): string[] { - return MOB_DEFINITIONS.map((m) => m.key); -} - -// --- DB-backed optional loader + simple validation --- -import { prisma } from "../../core/database/prisma"; -import { z } from "zod"; - -export const BaseMobDefinitionSchema = z.object({ - key: z.string(), - name: z.string(), - tier: z.number().int().nonnegative(), - base: z.object({ - hp: z.number(), - attack: z.number(), - defense: z.number().optional(), - }), - scaling: z - .object({ - hpPerLevel: z.number().optional(), - attackPerLevel: z.number().optional(), - defensePerLevel: z.number().optional(), - hpMultiplierPerTier: z.number().optional(), - }) - .optional(), - tags: z.array(z.string()).optional(), - rewardMods: z - .object({ - coinMultiplier: z.number().optional(), - extraDropChance: z.number().optional(), - }) - .optional(), - behavior: z - .object({ - maxRounds: z.number().optional(), - aggressive: z.boolean().optional(), - critChance: z.number().optional(), - critMultiplier: z.number().optional(), - }) - .optional(), -}); - -// Cache for DB-loaded definitions (key -> def) -const dbMobDefs: Record = {}; - -/** - * Try to refresh mob definitions from the database. This is optional and - * fails silently if the Prisma model/table doesn't exist or an error occurs. - * Call this during server startup to load editable mobs. - */ -export async function refreshMobDefinitionsFromDb() { - try { - // If no DB configured, skip - if (!process.env.XATA_DB) return; - const anyPrisma: any = prisma as any; - if (!anyPrisma.mob || typeof anyPrisma.mob.findMany !== "function") { - // Prisma model `mob` not present — skip quietly - return; - } - const rows = await anyPrisma.mob.findMany(); - // rows expected to contain a JSON/config column (we try `config` or `definition`) - const BaseMobDefinitionSchema = z.object({ - key: z.string(), - name: z.string(), - tier: z.number().int().nonnegative(), - base: z.object({ - hp: z.number(), - attack: z.number(), - defense: z.number().optional(), - }), - scaling: z - .object({ - hpPerLevel: z.number().optional(), - attackPerLevel: z.number().optional(), - defensePerLevel: z.number().optional(), - hpMultiplierPerTier: z.number().optional(), - }) - .optional(), - tags: z.array(z.string()).optional(), - rewardMods: z - .object({ - coinMultiplier: z.number().optional(), - extraDropChance: z.number().optional(), - }) - .optional(), - behavior: z - .object({ - maxRounds: z.number().optional(), - aggressive: z.boolean().optional(), - critChance: z.number().optional(), - critMultiplier: z.number().optional(), - }) - .optional(), - }); - - for (const r of rows) { - // Prisma model Mob stores arbitrary data in `metadata`, but some projects - // may place structured stats in `stats` or `drops`. Try those fields. - const cfg = - r.metadata ?? - r.stats ?? - r.drops ?? - r.config ?? - r.definition ?? - r.data ?? - null; - if (!cfg || typeof cfg !== "object") continue; - try { - const parsed = BaseMobDefinitionSchema.parse(cfg as any); - dbMobDefs[parsed.key] = parsed as BaseMobDefinition; - } catch (e) { - // eslint-disable-next-line no-console - console.warn( - "Invalid mob definition in DB for row id=", - r.id, - (e as any)?.message ?? e - ); - } - } - } catch (err) { - // silently ignore DB issues — keep in-memory definitions as source of truth - // but log to console for debugging - // eslint-disable-next-line no-console - console.warn( - "refreshMobDefinitionsFromDb: could not load mobs from DB:", - (err && (err as Error).message) || err - ); - } -} - -/** - * Find mob definition checking DB-loaded defs first, then built-in definitions. - */ -export function findMobDef(key: string) { - if (dbMobDefs[key]) return dbMobDefs[key]; - return MOB_DEFINITIONS.find((m) => m.key === key) || null; -} - -export function validateAllMobDefs() { - const bad: string[] = []; - for (const m of MOB_DEFINITIONS) { - const r = BaseMobDefinitionSchema.safeParse(m); - if (!r.success) bad.push(m.key ?? ""); - } - for (const k of Object.keys(dbMobDefs)) { - const r = BaseMobDefinitionSchema.safeParse(dbMobDefs[k]); - if (!r.success) bad.push(k); - } - if (bad.length) { - // eslint-disable-next-line no-console - console.warn("validateAllMobDefs: invalid mob defs:", bad); - } - return bad.length === 0; -} - -/** - * Initialize mob repository: attempt to refresh from DB and validate definitions. - * Call this on server start (optional). - */ -export async function initializeMobRepository() { - await refreshMobDefinitionsFromDb(); - validateAllMobDefs(); -} diff --git a/src/.backup/game_core/game/mutations/service.ts b/src/.backup/game_core/game/mutations/service.ts deleted file mode 100644 index e5657b7..0000000 --- a/src/.backup/game_core/game/mutations/service.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { prisma } from "../../core/database/prisma"; -import type { ItemProps } from "../economy/types"; -import { findItemByKey, getInventoryEntry } from "../economy/service"; -import { parseItemProps } from "../core/utils"; - -export async function findMutationByKey(guildId: string, key: string) { - return prisma.itemMutation.findFirst({ - where: { key, OR: [{ guildId }, { guildId: null }] }, - orderBy: [{ guildId: "desc" }], - }); -} - -export async function applyMutationToInventory( - userId: string, - guildId: string, - itemKey: string, - mutationKey: string -) { - const { item, entry } = await getInventoryEntry(userId, guildId, itemKey, { - createIfMissing: true, - }); - if (!entry) throw new Error("Inventario inexistente"); - - const props = parseItemProps(item.props); - const policy = props.mutationPolicy; - if (policy?.deniedKeys?.includes(mutationKey)) - throw new Error("Mutación denegada"); - if (policy?.allowedKeys && !policy.allowedKeys.includes(mutationKey)) - throw new Error("Mutación no permitida"); - - const mutation = await findMutationByKey(guildId, mutationKey); - if (!mutation) throw new Error("Mutación no encontrada"); - - await prisma.inventoryItemMutation.create({ - data: { inventoryId: entry.id, mutationId: mutation.id }, - }); - return { ok: true } as const; -} diff --git a/src/.backup/game_core/game/quests/service.ts b/src/.backup/game_core/game/quests/service.ts deleted file mode 100644 index 72ac6e6..0000000 --- a/src/.backup/game_core/game/quests/service.ts +++ /dev/null @@ -1,391 +0,0 @@ -import { prisma } from '../../core/database/prisma'; -import { giveRewards, type Reward } from '../rewards/service'; -import logger from '../../core/lib/logger'; -import { ensureUserAndGuildExist } from '../core/userService'; - -/** - * Actualizar progreso de misiones del jugador - */ -export async function updateQuestProgress( - userId: string, - guildId: string, - questType: string, - increment: number = 1 -) { - try { - // Asegurar que User y Guild existan antes de crear/buscar quest progress - await ensureUserAndGuildExist(userId, guildId); - - // Obtener misiones activas que coincidan con el tipo - const quests = await prisma.quest.findMany({ - where: { - OR: [{ guildId }, { guildId: null }], - active: true - } - }); - - const updates: any[] = []; - - for (const quest of quests) { - const req = quest.requirements as any; - - // Verificar si el tipo de misión coincide - if (req.type !== questType) continue; - - // Obtener o crear progreso - let progress = await prisma.questProgress.findFirst({ - where: { - userId, - guildId, - questId: quest.id, - claimed: false - }, - orderBy: { createdAt: 'desc' } - }); - - if (!progress) { - // Crear nuevo progreso - progress = await prisma.questProgress.create({ - data: { - userId, - guildId, - questId: quest.id, - progress: 0, - expiresAt: quest.endAt - } - }); - } - - // Ya completada y reclamada - if (progress.completed && progress.claimed) { - // Si es repetible, crear nuevo progreso - if (quest.repeatable) { - progress = await prisma.questProgress.create({ - data: { - userId, - guildId, - questId: quest.id, - progress: 0, - expiresAt: quest.endAt - } - }); - } else { - continue; - } - } - - // Actualizar progreso - const newProgress = progress.progress + increment; - const isCompleted = newProgress >= req.count; - - await prisma.questProgress.update({ - where: { id: progress.id }, - data: { - progress: newProgress, - completed: isCompleted, - completedAt: isCompleted ? new Date() : null - } - }); - - if (isCompleted) { - updates.push(quest); - } - } - - return updates; - } catch (error) { - console.error(`Error updating quest progress for ${userId}:`, error); - return []; - } -} - -/** - * Reclamar recompensa de misión completada - */ -export async function claimQuestReward( - userId: string, - guildId: string, - questId: string -) { - try { - const progress = await prisma.questProgress.findFirst({ - where: { - userId, - guildId, - questId, - completed: true, - claimed: false - }, - include: { - quest: true - } - }); - - if (!progress) { - throw new Error('Misión no encontrada o ya reclamada'); - } - - // Dar recompensas - const rewards = await giveRewards( - userId, - guildId, - progress.quest.rewards as Reward, - `quest:${progress.quest.key}` - ); - - // Marcar como reclamada - await prisma.questProgress.update({ - where: { id: progress.id }, - data: { - claimed: true, - claimedAt: new Date() - } - }); - - return { quest: progress.quest, rewards }; - } catch (error) { - console.error(`Error claiming quest reward for ${userId}:`, error); - throw error; - } -} - -/** - * Obtener misiones disponibles y progreso del jugador - */ -export async function getPlayerQuests(userId: string, guildId: string) { - const quests = await prisma.quest.findMany({ - where: { - OR: [{ guildId }, { guildId: null }], - active: true - }, - orderBy: [ - { type: 'asc' }, - { category: 'asc' } - ] - }); - - const questsWithProgress = await Promise.all( - quests.map(async (quest) => { - const progress = await prisma.questProgress.findFirst({ - where: { - userId, - guildId, - questId: quest.id - }, - orderBy: { createdAt: 'desc' } - }); - - return { - quest, - progress: progress || null, - canClaim: progress?.completed && !progress?.claimed, - percentage: progress - ? Math.min(100, Math.floor((progress.progress / (quest.requirements as any).count) * 100)) - : 0 - }; - }) - ); - - // Agrupar por tipo - return { - daily: questsWithProgress.filter(q => q.quest.type === 'daily'), - weekly: questsWithProgress.filter(q => q.quest.type === 'weekly'), - permanent: questsWithProgress.filter(q => q.quest.type === 'permanent'), - event: questsWithProgress.filter(q => q.quest.type === 'event') - }; -} - -/** - * Generar misiones diarias aleatorias - */ -export async function generateDailyQuests(guildId: string) { - try { - // Eliminar misiones diarias antiguas - await prisma.quest.deleteMany({ - where: { - guildId, - type: 'daily', - endAt: { lt: new Date() } - } - }); - - // Templates de misiones diarias expandidas - const dailyTemplates = [ - // Minería - { - key: 'daily_mine', - name: 'Minero Diario', - description: 'Mina 10 veces', - category: 'mining', - requirements: { type: 'mine_count', count: 10 }, - rewards: { coins: 500 } - }, - { - key: 'daily_mine_hard', - name: 'Minero Dedicado', - description: 'Mina 20 veces', - category: 'mining', - requirements: { type: 'mine_count', count: 20 }, - rewards: { coins: 1200 } - }, - // Pesca - { - key: 'daily_fish', - name: 'Pescador Diario', - description: 'Pesca 8 veces', - category: 'fishing', - requirements: { type: 'fish_count', count: 8 }, - rewards: { coins: 400 } - }, - { - key: 'daily_fish_hard', - name: 'Pescador Experto', - description: 'Pesca 15 veces', - category: 'fishing', - requirements: { type: 'fish_count', count: 15 }, - rewards: { coins: 900 } - }, - // Combate - { - key: 'daily_fight', - name: 'Guerrero Diario', - description: 'Pelea 5 veces', - category: 'combat', - requirements: { type: 'fight_count', count: 5 }, - rewards: { coins: 600 } - }, - { - key: 'daily_mob_slayer', - name: 'Cazador de Monstruos', - description: 'Derrota 10 mobs', - category: 'combat', - requirements: { type: 'mob_defeat_count', count: 10 }, - rewards: { coins: 800 } - }, - // Crafteo - { - key: 'daily_craft', - name: 'Artesano Diario', - description: 'Craftea 3 items', - category: 'crafting', - requirements: { type: 'craft_count', count: 3 }, - rewards: { coins: 300 } - }, - { - key: 'daily_craft_hard', - name: 'Maestro Artesano', - description: 'Craftea 10 items', - category: 'crafting', - requirements: { type: 'craft_count', count: 10 }, - rewards: { coins: 1000 } - }, - // Economía - { - key: 'daily_coins', - name: 'Acumulador', - description: 'Gana 5000 monedas', - category: 'economy', - requirements: { type: 'coins_earned', count: 5000 }, - rewards: { coins: 1000 } - }, - { - key: 'daily_purchase', - name: 'Comprador', - description: 'Compra 3 items en la tienda', - category: 'economy', - requirements: { type: 'items_purchased', count: 3 }, - rewards: { coins: 500 } - }, - // Items - { - key: 'daily_consume', - name: 'Consumidor', - description: 'Consume 5 items', - category: 'items', - requirements: { type: 'items_consumed', count: 5 }, - rewards: { coins: 300 } - }, - { - key: 'daily_equip', - name: 'Equipador', - description: 'Equipa 3 items diferentes', - category: 'items', - requirements: { type: 'items_equipped', count: 3 }, - rewards: { coins: 400 } - }, - // Fundición - { - key: 'daily_smelt', - name: 'Fundidor', - description: 'Funde 5 items', - category: 'smelting', - requirements: { type: 'items_smelted', count: 5 }, - rewards: { coins: 700 } - }, - // Combinadas - { - key: 'daily_variety', - name: 'Multitarea', - description: 'Mina, pesca y pelea 3 veces cada uno', - category: 'variety', - requirements: { - type: 'variety', - conditions: [ - { type: 'mine_count', count: 3 }, - { type: 'fish_count', count: 3 }, - { type: 'fight_count', count: 3 } - ] - }, - rewards: { coins: 1500 } - } - ]; - - // Crear 3 misiones diarias aleatorias - const selectedTemplates = dailyTemplates - .sort(() => Math.random() - 0.5) - .slice(0, 3); - - const startAt = new Date(); - const endAt = new Date(); - endAt.setHours(23, 59, 59, 999); - - for (const template of selectedTemplates) { - await prisma.quest.create({ - data: { - ...template, - guildId, - type: 'daily', - startAt, - endAt, - active: true, - repeatable: false - } - }); - } - - console.log(`Generated ${selectedTemplates.length} daily quests for guild ${guildId}`); - return selectedTemplates.length; - } catch (error) { - console.error(`Error generating daily quests for ${guildId}:`, error); - return 0; - } -} - -/** - * Limpiar misiones expiradas - */ -export async function cleanExpiredQuests(guildId: string) { - const result = await prisma.quest.updateMany({ - where: { - guildId, - active: true, - endAt: { lt: new Date() } - }, - data: { - active: false - } - }); - - console.log(`Deactivated ${result.count} expired quests for guild ${guildId}`); - return result.count; -} diff --git a/src/.backup/game_core/game/rewards/service.ts b/src/.backup/game_core/game/rewards/service.ts deleted file mode 100644 index 2ee796c..0000000 --- a/src/.backup/game_core/game/rewards/service.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { prisma } from '../../core/database/prisma'; -import { addItemByKey, adjustCoins } from '../economy/service'; -import logger from '../../core/lib/logger'; - -export interface Reward { - coins?: number; - items?: Array<{ key: string; quantity: number }>; - xp?: number; - title?: string; -} - -/** - * Dar recompensas a un jugador - */ -export async function giveRewards( - userId: string, - guildId: string, - rewards: Reward, - source: string -): Promise { - const results: string[] = []; - - try { - // Monedas - if (rewards.coins && rewards.coins > 0) { - await adjustCoins(userId, guildId, rewards.coins); - results.push(`💰 **${rewards.coins.toLocaleString()}** monedas`); - } - - // Items - if (rewards.items && rewards.items.length > 0) { - for (const item of rewards.items) { - await addItemByKey(userId, guildId, item.key, item.quantity); - results.push(`📦 **${item.quantity}x** ${item.key}`); - } - } - - // XP (por implementar si tienes sistema de XP) - if (rewards.xp && rewards.xp > 0) { - results.push(`⭐ **${rewards.xp}** XP`); - } - - // Título (por implementar) - if (rewards.title) { - results.push(`🏆 Título: **${rewards.title}**`); - } - - // Log de auditoría - await prisma.auditLog.create({ - data: { - userId, - guildId, - action: 'reward_given', - target: source, - details: rewards as any - } - }).catch(() => {}); // Silencioso si falla el log - - console.log(`Rewards given to ${userId} in ${guildId} from ${source}:`, rewards); - - return results; - } catch (error) { - console.error(`Error giving rewards to ${userId} in ${guildId}:`, error); - throw error; - } -} - -/** - * Validar que las recompensas sean válidas - */ -export function validateRewards(rewards: any): rewards is Reward { - if (typeof rewards !== 'object' || rewards === null) return false; - - if (rewards.coins !== undefined && (typeof rewards.coins !== 'number' || rewards.coins < 0)) { - return false; - } - - if (rewards.items !== undefined) { - if (!Array.isArray(rewards.items)) return false; - for (const item of rewards.items) { - if (!item.key || typeof item.key !== 'string') return false; - if (typeof item.quantity !== 'number' || item.quantity <= 0) return false; - } - } - - if (rewards.xp !== undefined && (typeof rewards.xp !== 'number' || rewards.xp < 0)) { - return false; - } - - if (rewards.title !== undefined && typeof rewards.title !== 'string') { - return false; - } - - return true; -} diff --git a/src/.backup/game_core/game/smelting/service.ts b/src/.backup/game_core/game/smelting/service.ts deleted file mode 100644 index 12f8165..0000000 --- a/src/.backup/game_core/game/smelting/service.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { prisma } from '../../core/database/prisma'; -import type { Prisma } from '@prisma/client'; -import { findItemByKey, addItemByKey } from '../economy/service'; - -export type SmeltInput = { itemKey: string; qty: number }; - -export async function createSmeltJob(userId: string, guildId: string, inputs: SmeltInput[], outputItemKey: string, outputQty: number, smeltSeconds: number) { - if (!inputs.length) throw new Error('No hay insumos'); - if (outputQty <= 0) throw new Error('Cantidad inválida'); - const readyAt = new Date(Date.now() + Math.max(1, smeltSeconds) * 1000); - - // Validar output item id - const outItem = await findItemByKey(guildId, outputItemKey); - if (!outItem) throw new Error('Output item no encontrado'); - - let newJobId: string | null = null; - // Validar y descontar inputs - await prisma.$transaction(async (tx) => { - // Chequeo - for (const i of inputs) { - const it = await tx.economyItem.findFirst({ where: { key: i.itemKey, OR: [{ guildId }, { guildId: null }] }, orderBy: [{ guildId: 'desc' }] }); - if (!it) throw new Error(`Input no encontrado: ${i.itemKey}`); - const inv = await tx.inventoryEntry.findUnique({ where: { userId_guildId_itemId: { userId, guildId, itemId: it.id } } }); - if ((inv?.quantity ?? 0) < i.qty) throw new Error(`Faltan insumos: ${i.itemKey}`); - } - // Descuento - for (const i of inputs) { - const it = await tx.economyItem.findFirst({ where: { key: i.itemKey, OR: [{ guildId }, { guildId: null }] }, orderBy: [{ guildId: 'desc' }] }); - if (!it) continue; - await tx.inventoryEntry.update({ where: { userId_guildId_itemId: { userId, guildId, itemId: it.id } }, data: { quantity: { decrement: i.qty } } }); - } - // Crear job - const created = await tx.smeltJob.create({ - data: { - userId, - guildId, - inputs: { items: inputs } as unknown as Prisma.InputJsonValue, - outputItemId: outItem.id, - outputQty, - readyAt, - status: 'pending', - }, - select: { id: true }, - }); - newJobId = created.id; - }); - - return { readyAt, jobId: newJobId! } as const; -} - -export async function claimSmeltJob(userId: string, guildId: string, jobId: string) { - const job = await prisma.smeltJob.findUnique({ where: { id: jobId } }); - if (!job || job.userId !== userId || job.guildId !== guildId) throw new Error('Job inválido'); - if (job.status !== 'pending' && job.status !== 'ready') throw new Error('Estado inválido'); - if (job.readyAt > new Date()) throw new Error('Aún no está listo'); - - await prisma.$transaction(async (tx) => { - await tx.smeltJob.update({ where: { id: job.id }, data: { status: 'claimed' } }); - const outItem = await tx.economyItem.findUnique({ where: { id: job.outputItemId } }); - if (outItem) { - const inv = await tx.inventoryEntry.findUnique({ where: { userId_guildId_itemId: { userId, guildId, itemId: outItem.id } } }); - if (inv) { - await tx.inventoryEntry.update({ where: { userId_guildId_itemId: { userId, guildId, itemId: outItem.id } }, data: { quantity: { increment: job.outputQty } } }); - } else { - await tx.inventoryEntry.create({ data: { userId, guildId, itemId: outItem.id, quantity: job.outputQty } }); - } - } - }); - - return { ok: true } as const; -} - -export async function claimNextReadyJob(userId: string, guildId: string) { - const job = await prisma.smeltJob.findFirst({ - where: { userId, guildId, status: { in: ['pending', 'ready'] }, readyAt: { lte: new Date() } }, - orderBy: { readyAt: 'asc' }, - }); - if (!job) throw new Error('No hay jobs listos'); - await claimSmeltJob(userId, guildId, job.id); - return { ok: true, jobId: job.id } as const; -} diff --git a/src/.backup/game_core/game/stats/service.ts b/src/.backup/game_core/game/stats/service.ts deleted file mode 100644 index 959a4d3..0000000 --- a/src/.backup/game_core/game/stats/service.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { prisma } from '../../core/database/prisma'; -import type { Prisma } from '@prisma/client'; -import logger from '../../core/lib/logger'; -import { ensureUserAndGuildExist } from '../core/userService'; - -/** - * Obtener o crear las estadísticas de un jugador - */ -export async function getOrCreatePlayerStats(userId: string, guildId: string) { - // Asegurar que User y Guild existan antes de crear/buscar stats - await ensureUserAndGuildExist(userId, guildId); - - let stats = await prisma.playerStats.findUnique({ - where: { userId_guildId: { userId, guildId } } - }); - - if (!stats) { - stats = await prisma.playerStats.create({ - data: { userId, guildId } - }); - } - - return stats; -} - -/** - * Actualizar estadísticas del jugador - */ -export async function updateStats( - userId: string, - guildId: string, - updates: Partial> -) { - try { - await getOrCreatePlayerStats(userId, guildId); - - const updateData: Prisma.PlayerStatsUpdateInput = {}; - - for (const [key, value] of Object.entries(updates)) { - if (value === undefined || value === null) continue; - - if (typeof value === 'number') { - (updateData as Record)[key] = { increment: value }; - } else { - (updateData as Record)[key] = value; - } - } - - const stats = await prisma.playerStats.update({ - where: { userId_guildId: { userId, guildId } }, - data: updateData - }); - - // Verificar récords - if (updates.damageDealt && typeof updates.damageDealt === 'number') { - if (updates.damageDealt > stats.highestDamageDealt) { - await prisma.playerStats.update({ - where: { userId_guildId: { userId, guildId } }, - data: { highestDamageDealt: updates.damageDealt } - }); - } - } - - return stats; - } catch (error) { - console.error("Error updating player stats", error); - throw error; - } -} - -/** - * Incrementar contador específico - */ -export async function incrementStat( - userId: string, - guildId: string, - stat: string, - amount: number = 1 -) { - await getOrCreatePlayerStats(userId, guildId); - - return await prisma.playerStats.update({ - where: { userId_guildId: { userId, guildId } }, - data: { [stat]: { increment: amount } } - }); -} - -/** - * Obtener leaderboard por categoría - */ -export async function getLeaderboard( - guildId: string, - category: keyof Omit, - limit: number = 10 -) { - const stats = await prisma.playerStats.findMany({ - where: { guildId }, - orderBy: { [category]: 'desc' }, - take: limit, - include: { - user: true - } - }); - - return stats.filter(s => (s[category] as number) > 0); -} - -/** - * Obtener estadísticas de un jugador con formato amigable - */ -export async function getPlayerStatsFormatted(userId: string, guildId: string) { - const stats = await getOrCreatePlayerStats(userId, guildId); - - return { - activities: { - '⛏️ Minas': stats.minesCompleted, - '🎣 Pesca': stats.fishingCompleted, - '⚔️ Combates': stats.fightsCompleted, - '🌾 Granja': stats.farmsCompleted - }, - combat: { - '👾 Mobs Derrotados': stats.mobsDefeated, - '💥 Daño Infligido': stats.damageDealt, - '🩹 Daño Recibido': stats.damageTaken, - '💀 Veces Derrotado': stats.timesDefeated, - '🏆 Racha de Victorias': stats.currentWinStreak, - '⭐ Mejor Racha': stats.longestWinStreak - }, - economy: { - '💰 Monedas Ganadas': stats.totalCoinsEarned, - '💸 Monedas Gastadas': stats.totalCoinsSpent, - '🛠️ Items Crafteados': stats.itemsCrafted, - '🔥 Items Fundidos': stats.itemsSmelted, - '🛒 Items Comprados': stats.itemsPurchased - }, - items: { - '📦 Cofres Abiertos': stats.chestsOpened, - '🍖 Items Consumidos': stats.itemsConsumed, - '⚔️ Items Equipados': stats.itemsEquipped - }, - records: { - '💥 Mayor Daño': stats.highestDamageDealt, - '💰 Más Monedas': stats.mostCoinsAtOnce - } - }; -} - -/** - * Resetear estadísticas de un jugador - */ -export async function resetPlayerStats(userId: string, guildId: string) { - return await prisma.playerStats.update({ - where: { userId_guildId: { userId, guildId } }, - data: { - minesCompleted: 0, - fishingCompleted: 0, - fightsCompleted: 0, - farmsCompleted: 0, - mobsDefeated: 0, - damageDealt: 0, - damageTaken: 0, - timesDefeated: 0, - totalCoinsEarned: 0, - totalCoinsSpent: 0, - itemsCrafted: 0, - itemsSmelted: 0, - itemsPurchased: 0, - chestsOpened: 0, - itemsConsumed: 0, - itemsEquipped: 0, - highestDamageDealt: 0, - longestWinStreak: 0, - currentWinStreak: 0, - mostCoinsAtOnce: 0 - } - }); -} diff --git a/src/.backup/game_core/game/stats/types.ts b/src/.backup/game_core/game/stats/types.ts deleted file mode 100644 index 2700f10..0000000 --- a/src/.backup/game_core/game/stats/types.ts +++ /dev/null @@ -1,41 +0,0 @@ -export interface StatsUpdate { - // Minijuegos - minesCompleted?: number; - fishingCompleted?: number; - fightsCompleted?: number; - farmsCompleted?: number; - - // Combate - mobsDefeated?: number; - damageDealt?: number; - damageTaken?: number; - timesDefeated?: number; - - // Economía - totalCoinsEarned?: number; - totalCoinsSpent?: number; - itemsCrafted?: number; - itemsSmelted?: number; - itemsPurchased?: number; - - // Items - chestsOpened?: number; - itemsConsumed?: number; - itemsEquipped?: number; - - // Récords - highestDamageDealt?: number; - longestWinStreak?: number; - currentWinStreak?: number; - mostCoinsAtOnce?: number; -} - -export type StatCategory = - | 'minesCompleted' - | 'fishingCompleted' - | 'fightsCompleted' - | 'farmsCompleted' - | 'mobsDefeated' - | 'damageDealt' - | 'totalCoinsEarned' - | 'itemsCrafted'; diff --git a/src/.backup/game_core/game/streaks/service.ts b/src/.backup/game_core/game/streaks/service.ts deleted file mode 100644 index 345900c..0000000 --- a/src/.backup/game_core/game/streaks/service.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { prisma } from '../../core/database/prisma'; -import { giveRewards, type Reward } from '../rewards/service'; -import logger from '../../core/lib/logger'; -import { ensureUserAndGuildExist } from '../core/userService'; - -/** - * Obtener o crear racha del jugador - */ -export async function getOrCreateStreak(userId: string, guildId: string) { - // Asegurar que User y Guild existan antes de crear/buscar streak - await ensureUserAndGuildExist(userId, guildId); - - let streak = await prisma.playerStreak.findUnique({ - where: { userId_guildId: { userId, guildId } } - }); - - if (!streak) { - streak = await prisma.playerStreak.create({ - data: { - userId, - guildId, - currentStreak: 0, - longestStreak: 0, - lastActiveDate: new Date(), - totalDaysActive: 0 - } - }); - } - - return streak; -} - -/** - * Actualizar racha diaria del jugador - */ -export async function updateStreak(userId: string, guildId: string) { - try { - const streak = await getOrCreateStreak(userId, guildId); - const now = new Date(); - const lastActive = new Date(streak.lastActiveDate); - - // Resetear hora para comparar solo la fecha - const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const lastDay = new Date(lastActive.getFullYear(), lastActive.getMonth(), lastActive.getDate()); - - const daysDiff = Math.floor((today.getTime() - lastDay.getTime()) / (1000 * 60 * 60 * 24)); - - // Ya actualizó hoy - if (daysDiff === 0) { - return { streak, newDay: false, rewards: null }; - } - - let newStreak = streak.currentStreak; - - // Si pasó 1 día exacto, incrementar racha - if (daysDiff === 1) { - newStreak = streak.currentStreak + 1; - } - // Si pasó más de 1 día, resetear racha - else if (daysDiff > 1) { - newStreak = 1; - } - - // Actualizar longest streak si es necesario - const newLongest = Math.max(streak.longestStreak, newStreak); - - const updated = await prisma.playerStreak.update({ - where: { userId_guildId: { userId, guildId } }, - data: { - currentStreak: newStreak, - longestStreak: newLongest, - lastActiveDate: now, - totalDaysActive: { increment: 1 } - } - }); - - // Obtener recompensa del día - const reward = getStreakReward(newStreak); - - // Dar recompensas - if (reward) { - await giveRewards(userId, guildId, reward, `streak:day${newStreak}`); - } - - return { - streak: updated, - newDay: true, - rewards: reward, - daysIncreased: daysDiff === 1 - }; - } catch (error) { - console.error(`Error updating streak for ${userId}:`, error); - throw error; - } -} - -/** - * Obtener recompensa según el día de racha - */ -export function getStreakReward(day: number): Reward | null { - // Recompensas especiales por día - const specialDays: Record = { - 1: { coins: 100 }, - 3: { coins: 300 }, - 5: { coins: 500 }, - 7: { coins: 1000 }, - 10: { coins: 1500 }, - 14: { coins: 2500 }, - 21: { coins: 5000 }, - 30: { coins: 10000 }, - 60: { coins: 25000 }, - 90: { coins: 50000 }, - 180: { coins: 100000 }, - 365: { coins: 500000 } - }; - - // Si hay recompensa especial para este día - if (specialDays[day]) { - return specialDays[day]; - } - - // Recompensa base diaria (escala con el día) - const baseCoins = 50; - const bonus = Math.floor(day / 7) * 50; // +50 monedas cada 7 días - - return { - coins: baseCoins + bonus - }; -} - -/** - * Obtener información de la racha con recompensas próximas - */ -export async function getStreakInfo(userId: string, guildId: string) { - const streak = await getOrCreateStreak(userId, guildId); - const now = new Date(); - const lastActive = new Date(streak.lastActiveDate); - - const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const lastDay = new Date(lastActive.getFullYear(), lastActive.getMonth(), lastActive.getDate()); - - const daysDiff = Math.floor((today.getTime() - lastDay.getTime()) / (1000 * 60 * 60 * 24)); - - // Calcular si la racha está activa o expiró - const isActive = daysDiff <= 1; - const willExpireSoon = daysDiff === 0; // Ya jugó hoy - - // Próxima recompensa especial - const specialDays = [1, 3, 5, 7, 10, 14, 21, 30, 60, 90, 180, 365]; - const nextMilestone = specialDays.find(d => d > streak.currentStreak) || null; - - return { - streak, - isActive, - willExpireSoon, - daysDiff, - nextMilestone, - nextMilestoneIn: nextMilestone ? nextMilestone - streak.currentStreak : null, - todayReward: getStreakReward(streak.currentStreak + 1) - }; -} - -/** - * Resetear racha de un jugador (admin) - */ -export async function resetStreak(userId: string, guildId: string) { - return await prisma.playerStreak.update({ - where: { userId_guildId: { userId, guildId } }, - data: { - currentStreak: 0, - lastActiveDate: new Date() - } - }); -} diff --git a/src/.backup/itemCreate.ts.backup2 b/src/.backup/itemCreate.ts.backup2 deleted file mode 100644 index c585623..0000000 --- a/src/.backup/itemCreate.ts.backup2 +++ /dev/null @@ -1,227 +0,0 @@ -import { Message, MessageFlags, MessageComponentInteraction, ButtonInteraction, TextBasedChannel } from 'discord.js'; -import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10'; -import type { CommandMessage } from '../../../core/types/commands'; -import { hasManageGuildOrStaff } from '../../../core/lib/permissions'; -import logger from '../../../core/lib/logger'; -import type Amayo from '../../../core/client'; - -interface ItemEditorState { - key: string; - name?: string; - description?: string; - category?: string; - icon?: string; - stackable?: boolean; - maxPerInventory?: number | null; - tags: string[]; - props?: any; -} - -export const command: CommandMessage = { - name: 'item-crear', - type: 'message', - aliases: ['crear-item','itemcreate'], - cooldown: 10, - description: 'Crea un EconomyItem para este servidor con un pequeño editor interactivo.', - category: 'Economía', - usage: 'item-crear ', - run: async (message: Message, args: string[], client: Amayo) => { - const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma); - if (!allowed) { - await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.'); - return; - } - - const key = args[0]?.trim(); - if (!key) { - await message.reply('Uso: `!item-crear `'); - return; - } - - const guildId = message.guild!.id; - - const exists = await client.prisma.economyItem.findFirst({ where: { key, guildId } }); - if (exists) { - await message.reply('❌ Ya existe un item con esa key en este servidor.'); - return; - } - - const state: ItemEditorState = { - key, - tags: [], - stackable: true, - maxPerInventory: null, - props: {}, - }; - - const channel = message.channel as TextBasedChannel & { send: Function }; - const editorMsg = await channel.send({ - content: `🛠️ Editor de Item: \`${key}\`\nUsa los botones para configurar los campos y luego guarda.`, - components: [ - { type: 1, components: [ - { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'it_base' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Tags', custom_id: 'it_tags' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Props (JSON)', custom_id: 'it_props' }, - { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'it_save' }, - { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'it_cancel' }, - ]}, - ], - }); - - const collector = editorMsg.createMessageComponentCollector({ time: 30 * 60_000, filter: (i) => i.user.id === message.author.id }); - - collector.on('collect', async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - if (i.customId === 'it_cancel') { - await i.deferUpdate(); - await editorMsg.edit({ content: '❌ Editor cancelado.', components: [] }); - collector.stop('cancel'); - return; - } - if (i.customId === 'it_base') { - await showBaseModal(i as ButtonInteraction, state); - return; - } - if (i.customId === 'it_tags') { - await showTagsModal(i as ButtonInteraction, state); - return; - } - if (i.customId === 'it_props') { - await showPropsModal(i as ButtonInteraction, state); - return; - } - if (i.customId === 'it_save') { - // Validar - if (!state.name) { - await i.reply({ content: '❌ Falta el nombre del item (configura en Base).', flags: MessageFlags.Ephemeral }); - return; - } - // Guardar - await client.prisma.economyItem.create({ - data: { - guildId, - key: state.key, - name: state.name!, - description: state.description, - category: state.category, - icon: state.icon, - stackable: state.stackable ?? true, - maxPerInventory: state.maxPerInventory ?? undefined, - tags: state.tags, - props: state.props ?? {}, - }, - }); - await i.reply({ content: '✅ Item guardado!', flags: MessageFlags.Ephemeral }); - await editorMsg.edit({ content: `✅ Item \`${state.key}\` creado.`, components: [] }); - collector.stop('saved'); - return; - } - } catch (err) { - logger.error({ err }, 'item-crear interaction error'); - if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral }); - } - }); - - collector.on('end', async (_c, r) => { - if (r === 'time') { - try { await editorMsg.edit({ content: '⏰ Editor expirado.', components: [] }); } catch {} - } - }); - }, -}; - -async function showBaseModal(i: ButtonInteraction, state: ItemEditorState) { - const modal = { - title: 'Configuración base del Item', - customId: 'it_base_modal', - components: [ - { type: ComponentType.Label, label: 'Nombre', component: { type: ComponentType.TextInput, customId: 'name', style: TextInputStyle.Short, required: true, value: state.name ?? '' } }, - { type: ComponentType.Label, label: 'Descripción', component: { type: ComponentType.TextInput, customId: 'desc', style: TextInputStyle.Paragraph, required: false, value: state.description ?? '' } }, - { type: ComponentType.Label, label: 'Categoría', component: { type: ComponentType.TextInput, customId: 'cat', style: TextInputStyle.Short, required: false, value: state.category ?? '' } }, - { type: ComponentType.Label, label: 'Icon URL', component: { type: ComponentType.TextInput, customId: 'icon', style: TextInputStyle.Short, required: false, value: state.icon ?? '' } }, - { type: ComponentType.Label, label: 'Stackable y Máx inventario', component: { type: ComponentType.TextInput, customId: 'stack_max', style: TextInputStyle.Short, required: false, placeholder: 'true,10', value: state.stackable !== undefined ? `${state.stackable},${state.maxPerInventory ?? ''}` : '' } }, - ], - } as const; - - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - const name = sub.components.getTextInputValue('name').trim(); - const desc = sub.components.getTextInputValue('desc').trim(); - const cat = sub.components.getTextInputValue('cat').trim(); - const icon = sub.components.getTextInputValue('icon').trim(); - const stackMax = sub.components.getTextInputValue('stack_max').trim(); - - state.name = name; - state.description = desc || undefined; - state.category = cat || undefined; - state.icon = icon || undefined; - - if (stackMax) { - const [s, m] = stackMax.split(','); - state.stackable = String(s).toLowerCase() !== 'false'; - const mv = m?.trim(); - state.maxPerInventory = mv ? Math.max(0, parseInt(mv, 10) || 0) : null; - } - - await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral }); - } catch {} -} - -async function showTagsModal(i: ButtonInteraction, state: ItemEditorState) { - const modal = { - title: 'Tags del Item (separados por coma)', - customId: 'it_tags_modal', - components: [ - { type: ComponentType.Label, label: 'Tags', component: { type: ComponentType.TextInput, customId: 'tags', style: TextInputStyle.Paragraph, required: false, value: state.tags.join(', ') } }, - ], - } as const; - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - const tags = sub.components.getTextInputValue('tags'); - state.tags = tags ? tags.split(',').map((t) => t.trim()).filter(Boolean) : []; - await sub.reply({ content: '✅ Tags actualizados.', flags: MessageFlags.Ephemeral }); - } catch {} -} - -async function showPropsModal(i: ButtonInteraction, state: ItemEditorState) { - const template = state.props && Object.keys(state.props).length ? JSON.stringify(state.props) : JSON.stringify({ - tool: undefined, - breakable: undefined, - chest: undefined, - eventCurrency: undefined, - passiveEffects: [], - mutationPolicy: undefined, - craftingOnly: false, - food: undefined, - damage: undefined, - defense: undefined, - maxHpBonus: undefined, - }); - const modal = { - title: 'Props (JSON) del Item', - customId: 'it_props_modal', - components: [ - { type: ComponentType.Label, label: 'JSON', component: { type: ComponentType.TextInput, customId: 'props', style: TextInputStyle.Paragraph, required: false, value: template.slice(0,4000) } }, - ], - } as const; - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - const raw = sub.components.getTextInputValue('props'); - if (raw) { - try { - const parsed = JSON.parse(raw); - state.props = parsed; - await sub.reply({ content: '✅ Props guardados.', flags: MessageFlags.Ephemeral }); - } catch (e) { - await sub.reply({ content: '❌ JSON inválido.', flags: MessageFlags.Ephemeral }); - } - } else { - state.props = {}; - await sub.reply({ content: 'ℹ️ Props limpiados.', flags: MessageFlags.Ephemeral }); - } - } catch {} -} diff --git a/src/.backup/itemEdit.ts.backup2 b/src/.backup/itemEdit.ts.backup2 deleted file mode 100644 index a409295..0000000 --- a/src/.backup/itemEdit.ts.backup2 +++ /dev/null @@ -1,143 +0,0 @@ -import { Message, MessageFlags, MessageComponentInteraction, ButtonInteraction, TextBasedChannel } from 'discord.js'; -import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10'; -import type { CommandMessage } from '../../../core/types/commands'; -import { hasManageGuildOrStaff } from '../../../core/lib/permissions'; -import logger from '../../../core/lib/logger'; -import type Amayo from '../../../core/client'; - -interface ItemEditorState { - key: string; - name?: string; - description?: string; - category?: string; - icon?: string; - stackable?: boolean; - maxPerInventory?: number | null; - tags: string[]; - props?: any; -} - -export const command: CommandMessage = { - name: 'item-editar', - type: 'message', - aliases: ['editar-item','itemedit'], - cooldown: 10, - description: 'Edita un EconomyItem de este servidor con un editor interactivo.', - category: 'Economía', - usage: 'item-editar ', - run: async (message: Message, args: string[], client: Amayo) => { - const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma); - if (!allowed) { await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.'); return; } - const key = args[0]?.trim(); - if (!key) { await message.reply('Uso: `!item-editar `'); return; } - const guildId = message.guild!.id; - - const item = await client.prisma.economyItem.findFirst({ where: { key, guildId } }); - if (!item) { await message.reply('❌ No existe un item con esa key en este servidor.'); return; } - - const state: ItemEditorState = { - key, - name: item.name, - description: item.description ?? undefined, - category: item.category ?? undefined, - icon: item.icon ?? undefined, - stackable: item.stackable ?? true, - maxPerInventory: item.maxPerInventory ?? null, - tags: item.tags ?? [], - props: item.props ?? {}, - }; - - const channel = message.channel as TextBasedChannel & { send: Function }; - const editorMsg = await channel.send({ - content: `🛠️ Editor de Item (editar): \`${key}\``, - components: [ { type: 1, components: [ - { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'it_base' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Tags', custom_id: 'it_tags' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Props (JSON)', custom_id: 'it_props' }, - { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'it_save' }, - { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'it_cancel' }, - ] } ], - }); - - const collector = editorMsg.createMessageComponentCollector({ time: 30 * 60_000, filter: (i) => i.user.id === message.author.id }); - - collector.on('collect', async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - if (i.customId === 'it_cancel') { await i.deferUpdate(); await editorMsg.edit({ content: '❌ Editor cancelado.', components: [] }); collector.stop('cancel'); return; } - if (i.customId === 'it_base') { await showBaseModal(i as ButtonInteraction, state); return; } - if (i.customId === 'it_tags') { await showTagsModal(i as ButtonInteraction, state); return; } - if (i.customId === 'it_props') { await showPropsModal(i as ButtonInteraction, state); return; } - if (i.customId === 'it_save') { - if (!state.name) { await i.reply({ content: '❌ Falta el nombre del item.', flags: MessageFlags.Ephemeral }); return; } - await client.prisma.economyItem.update({ - where: { id: item.id }, - data: { - name: state.name!, - description: state.description, - category: state.category, - icon: state.icon, - stackable: state.stackable ?? true, - maxPerInventory: state.maxPerInventory ?? undefined, - tags: state.tags, - props: state.props ?? {}, - }, - }); - await i.reply({ content: '✅ Item actualizado!', flags: MessageFlags.Ephemeral }); - await editorMsg.edit({ content: `✅ Item \`${state.key}\` actualizado.`, components: [] }); - collector.stop('saved'); - return; - } - } catch (err) { - logger.error({ err }, 'item-editar interaction error'); - if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral }); - } - }); - - collector.on('end', async (_c, r) => { if (r === 'time') { try { await editorMsg.edit({ content: '⏰ Editor expirado.', components: [] }); } catch {} } }); - }, -}; - -async function showBaseModal(i: ButtonInteraction, state: ItemEditorState) { - const modal = { - title: 'Configuración base del Item', customId: 'it_base_modal', components: [ - { type: ComponentType.Label, label: 'Nombre', component: { type: ComponentType.TextInput, customId: 'name', style: TextInputStyle.Short, required: true, value: state.name ?? '' } }, - { type: ComponentType.Label, label: 'Descripción', component: { type: ComponentType.TextInput, customId: 'desc', style: TextInputStyle.Paragraph, required: false, value: state.description ?? '' } }, - { type: ComponentType.Label, label: 'Categoría', component: { type: ComponentType.TextInput, customId: 'cat', style: TextInputStyle.Short, required: false, value: state.category ?? '' } }, - { type: ComponentType.Label, label: 'Icon URL', component: { type: ComponentType.TextInput, customId: 'icon', style: TextInputStyle.Short, required: false, value: state.icon ?? '' } }, - { type: ComponentType.Label, label: 'Stackable y Máx inventario', component: { type: ComponentType.TextInput, customId: 'stack_max', style: TextInputStyle.Short, required: false, placeholder: 'true,10', value: state.stackable !== undefined ? `${state.stackable},${state.maxPerInventory ?? ''}` : '' } }, - ], } as const; - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - state.name = sub.components.getTextInputValue('name').trim(); - state.description = sub.components.getTextInputValue('desc').trim() || undefined; - state.category = sub.components.getTextInputValue('cat').trim() || undefined; - state.icon = sub.components.getTextInputValue('icon').trim() || undefined; - const stackMax = sub.components.getTextInputValue('stack_max').trim(); - if (stackMax) { const [s,m] = stackMax.split(','); state.stackable = String(s).toLowerCase() !== 'false'; const mv = m?.trim(); state.maxPerInventory = mv ? Math.max(0, parseInt(mv,10)||0) : null; } - await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral }); - } catch {} -} - -async function showTagsModal(i: ButtonInteraction, state: ItemEditorState) { - const modal = { title: 'Tags del Item (separados por coma)', customId: 'it_tags_modal', components: [ - { type: ComponentType.Label, label: 'Tags', component: { type: ComponentType.TextInput, customId: 'tags', style: TextInputStyle.Paragraph, required: false, value: state.tags.join(', ') } }, - ], } as const; - await i.showModal(modal); - try { const sub = await i.awaitModalSubmit({ time: 300_000 }); const tags = sub.components.getTextInputValue('tags'); state.tags = tags ? tags.split(',').map(t=>t.trim()).filter(Boolean) : []; await sub.reply({ content: '✅ Tags actualizados.', flags: MessageFlags.Ephemeral }); } catch {} -} - -async function showPropsModal(i: ButtonInteraction, state: ItemEditorState) { - const template = state.props && Object.keys(state.props).length ? JSON.stringify(state.props) : JSON.stringify({}); - const modal = { title: 'Props (JSON) del Item', customId: 'it_props_modal', components: [ - { type: ComponentType.Label, label: 'JSON', component: { type: ComponentType.TextInput, customId: 'props', style: TextInputStyle.Paragraph, required: false, value: template.slice(0,4000) } }, - ], } as const; - await i.showModal(modal); - try { - const sub = await i.awaitModalSubmit({ time: 300_000 }); - const raw = sub.components.getTextInputValue('props'); - if (raw) { try { state.props = JSON.parse(raw); await sub.reply({ content: '✅ Props guardados.', flags: MessageFlags.Ephemeral }); } catch { await sub.reply({ content: '❌ JSON inválido.', flags: MessageFlags.Ephemeral }); } } - else { state.props = {}; await sub.reply({ content: 'ℹ️ Props limpiados.', flags: MessageFlags.Ephemeral }); } - } catch {} -} diff --git a/src/.backup/listChannels.backup.ts.txt b/src/.backup/listChannels.backup.ts.txt deleted file mode 100644 index 80aba7d..0000000 --- a/src/.backup/listChannels.backup.ts.txt +++ /dev/null @@ -1,346 +0,0 @@ -import { CommandMessage } from "../core/types/commands"; -// @ts-ignore -import { EmbedBuilder, ButtonStyle, MessageFlags, ChannelType } from "discord.js"; - -export const command: CommandMessage = { - name: "listar-canales-alianza", - type: "message", - aliases: ["listchannels-alliance", "listalchannel", "channelsally", "alliancechannels"], - cooldown: 5, - // @ts-ignore - run: async (message, args, client) => { - // Obtener canales configurados existentes con estadísticas - const existingChannels = await client.prisma.allianceChannel.findMany({ - where: { guildId: message.guildId! }, - include: { - _count: { - select: { - pointsHistory: true - } - } - }, - orderBy: { - createdAt: 'desc' - } - }); - - // Obtener estadísticas generales - const totalPointsHistory = await client.prisma.pointHistory.count({ - where: { guildId: message.guildId! } - }); - - const availableBlocks = await client.prisma.blockV2Config.count({ - where: { guildId: message.guildId! } - }); - - if (existingChannels.length === 0) { - // Embed cuando no hay canales configurados - const noChannelsEmbed = new EmbedBuilder() - .setTitle("📋 Canales de Alianza Configurados") - .setDescription("```\n🗂️ Lista vacía\n```\n\n📭 **No hay canales configurados** para alianzas en este servidor.\n\n🚀 **¿Quieres empezar?**\n• Usa `!setchannel-alliance` para configurar tu primer canal\n• Crea bloques con `!blockcreate `") - .setColor(0x36393f) - .addFields([ - { - name: "📊 Estadísticas Generales", - value: `🧩 **Bloques disponibles:** ${availableBlocks}\n📈 **Puntos totales otorgados:** ${totalPointsHistory}`, - inline: false - } - ]) - .setFooter({ - text: `📅 ${new Date().toLocaleDateString('es-ES', { - year: 'numeric', - month: 'long', - day: 'numeric' - })}` - }) - .setTimestamp(); - - const helpRow = { - type: 1, - components: [ - { - type: 2, - style: ButtonStyle.Success, - label: "➕ Configurar Canal", - custom_id: "setup_first_channel", - emoji: { name: "🔧" } - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "📖 Ayuda", - custom_id: "show_setup_help", - emoji: { name: "❓" } - } - ] - }; - - const response = await message.reply({ - embeds: [noChannelsEmbed], - components: [helpRow] - }); - - // Collector para botones de ayuda - const collector = response.createMessageComponentCollector({ - time: 300000, - filter: (i) => i.user.id === message.author.id - }); - - collector.on("collect", async (interaction) => { - if (interaction.customId === "setup_first_channel") { - await interaction.reply({ - content: "🔧 **Configurar Canal**\n\nUsa el comando: `!setchannel-alliance`\n\nEste comando te guiará paso a paso para configurar tu primer canal de alianzas.", - flags: 64 // Ephemeral - }); - } else if (interaction.customId === "show_setup_help") { - await interaction.reply({ - content: "📖 **Guía de Configuración**\n\n**Paso 1:** Crear un bloque\n`!blockcreate mi-alianza`\n\n**Paso 2:** Configurar canal\n`!setchannel-alliance`\n\n**Paso 3:** ¡Listo!\nLos usuarios ganarán puntos automáticamente.", - flags: 64 // Ephemeral - }); - } - }); - - return; - } - - // Crear descripción detallada de canales - let channelListDescription = "```\n📋 Lista de Canales Configurados\n```\n\n"; - - const channelDetails = await Promise.all( - existingChannels.map(async (config: any, index: number) => { - const channel = message.guild!.channels.cache.get(config.channelId); - const channelName = channel ? `#${channel.name}` : "❌ *Canal Eliminado*"; - const status = config.isActive ? "🟢 **Activo**" : "🔴 **Inactivo**"; - const pointsCount = config._count.pointsHistory; - - // Obtener información del bloque - const blockInfo = await client.prisma.blockV2Config.findFirst({ - where: { - guildId: message.guildId!, - name: config.blockConfigName - }, - select: { name: true, id: true } - }); - - const blockStatus = blockInfo ? "✅ Válido" : "⚠️ Bloque Eliminado"; - - const createdDate = new Date(config.createdAt).toLocaleDateString('es-ES', { - day: '2-digit', - month: '2-digit', - year: 'numeric' - }); - - return { - index: index + 1, - channelName, - status, - pointsCount, - blockName: config.blockConfigName, - blockStatus, - createdDate, - isValid: !!channel && !!blockInfo - }; - }) - ); - - // Agrupar por estado - //@ts-ignore - const activeChannels = channelDetails.filter(c => c.status.includes("Activo")); - //@ts-ignore - const inactiveChannels = channelDetails.filter(c => c.status.includes("Inactivo")); - - // Construir embed principal - const mainEmbed = new EmbedBuilder() - .setTitle("📋 Canales de Alianza Configurados") - .setDescription(`${channelListDescription}📊 **Resumen:** ${existingChannels.length} canal(es) configurado(s)\n🟢 **Activos:** ${activeChannels.length} • 🔴 **Inactivos:** ${inactiveChannels.length}`) - .setColor(0x5865f2) - .setThumbnail(message.guild!.iconURL({ size: 128 }) || null) - .setFooter({ - text: `📅 Actualizado • ${message.guild!.name}`, - iconURL: message.guild!.iconURL({ size: 32 }) || undefined - }) - .setTimestamp(); - - // Añadir campos de canales activos - if (activeChannels.length > 0) { - //@ts-ignore - const activeList = activeChannels.slice(0, 10).map(c => - `**${c.index}.** ${c.channelName}\n` + - `└ \`${c.blockName}\` • ${c.blockStatus}\n` + - `└ 📈 **${c.pointsCount}** puntos otorgados\n` + - `└ 📅 Desde: ${c.createdDate}` - ).join('\n\n'); - - mainEmbed.addFields([ - { - name: `🟢 Canales Activos (${activeChannels.length})`, - value: activeList || "Ninguno", - inline: false - } - ]); - } - - // Añadir campos de canales inactivos (si los hay) - if (inactiveChannels.length > 0) { - //@ts-ignore - const inactiveList = inactiveChannels.slice(0, 5).map(c => - `**${c.index}.** ${c.channelName}\n` + - `└ \`${c.blockName}\` • ${c.blockStatus}` - ).join('\n\n'); - - mainEmbed.addFields([ - { - name: `🔴 Canales Inactivos (${inactiveChannels.length})`, - value: inactiveList || "Ninguno", - inline: false - } - ]); - } - - // Añadir estadísticas generales - mainEmbed.addFields([ - { - name: "📊 Estadísticas del Servidor", - //@ts-ignore - value: `🧩 **Bloques disponibles:** ${availableBlocks}\n📈 **Total puntos otorgados:** ${totalPointsHistory}\n⚡ **Canales más activos:** ${channelDetails.sort((a, b) => b.pointsCount - a.pointsCount).slice(0, 3).map((c, i) => `${i + 1}. ${c.channelName.replace(/[#❌*]/g, '').trim()}`).join(', ') || 'N/A'}`, - inline: false - } - ]); - - // Botones de acción - const actionRow1 = { - type: 1, - components: [ - { - type: 2, - style: ButtonStyle.Success, - label: "➕ Añadir Canal", - custom_id: "add_channel", - emoji: { name: "🔧" } - }, - { - type: 2, - style: ButtonStyle.Danger, - label: "🗑️ Eliminar Canal", - custom_id: "remove_channel", - emoji: { name: "🗑️" } - }, - { - type: 2, - style: ButtonStyle.Primary, - label: "🔄 Actualizar", - custom_id: "refresh_list", - emoji: { name: "🔄" } - } - ] - }; - - const actionRow2 = { - type: 1, - components: [ - { - type: 2, - style: ButtonStyle.Secondary, - label: "📊 Estadísticas", - custom_id: "show_stats", - emoji: { name: "📈" } - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "🧩 Ver Bloques", - custom_id: "show_blocks", - emoji: { name: "🧩" } - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "❓ Ayuda", - custom_id: "show_help", - emoji: { name: "📖" } - } - ] - }; - - const response = await message.reply({ - embeds: [mainEmbed], - components: [actionRow1, actionRow2] - }); - - // Collector para manejar interacciones - const collector = response.createMessageComponentCollector({ - time: 600000, // 10 minutos - filter: (i) => i.user.id === message.author.id - }); - - collector.on("collect", async (interaction) => { - switch (interaction.customId) { - case "add_channel": - await interaction.reply({ - content: "➕ **Añadir Canal**\n\nUsa el comando: `!setchannel-alliance`\n\nEste comando te guiará para configurar un nuevo canal de alianzas.", - flags: 64 // Ephemeral - }); - break; - - case "remove_channel": - await interaction.reply({ - content: "🗑️ **Eliminar Canal**\n\nUsa el comando: `!removechannel-alliance`\n\nEste comando te permitirá eliminar canales de la configuración de alianzas.", - flags: 64 // Ephemeral - }); - break; - - case "refresh_list": - await interaction.reply({ - content: "🔄 **Lista Actualizada**\n\nUsa el comando nuevamente: `!listchannels-alliance`\n\nEsto mostrará la información más reciente.", - flags: 64 // Ephemeral - }); - break; - - case "show_stats": - //@ts-ignore - const detailedStats = channelDetails.map(c => - `• ${c.channelName}: **${c.pointsCount}** puntos` - ).join('\n'); - - await interaction.reply({ - content: `📊 **Estadísticas Detalladas**\n\n**Puntos por Canal:**\n${detailedStats}\n\n**Total del Servidor:** ${totalPointsHistory} puntos`, - flags: 64 // Ephemeral - }); - break; - - case "show_blocks": - const blocksList = await client.prisma.blockV2Config.findMany({ - where: { guildId: message.guildId! }, - select: { name: true, id: true } - }); - - const blocksText = blocksList.length > 0 - ? blocksList.map((block: any, i: number) => `${i + 1}. \`${block.name}\``).join('\n') - : "No hay bloques configurados"; - - await interaction.reply({ - content: `🧩 **Bloques Disponibles (${blocksList.length})**\n\n${blocksText}\n\n💡 Crea bloques con: \`!blockcreate \``, - flags: 64 // Ephemeral - }); - break; - - case "show_help": - await interaction.reply({ - content: `📖 **Ayuda - Sistema de Alianzas**\n\n**Comandos principales:**\n• \`!setchannel-alliance\` - Configurar canal\n• \`!removechannel-alliance\` - Eliminar canal\n• \`!listchannels-alliance\` - Ver configurados\n\n**Comandos de bloques:**\n• \`!blockcreate \` - Crear bloque\n• \`!blockeditv2 \` - Editar bloque\n• \`!embedlist\` - Ver todos los bloques`, - flags: 64 // Ephemeral - }); - break; - } - }); - - collector.on("end", async () => { - try { - await response.edit({ - components: [] // Remover botones cuando expire - }); - } catch (error) { - // Ignorar errores si el mensaje fue eliminado - } - }); - } -} diff --git a/src/.backup/mobCreate.ts.backup2 b/src/.backup/mobCreate.ts.backup2 deleted file mode 100644 index a1a757e..0000000 --- a/src/.backup/mobCreate.ts.backup2 +++ /dev/null @@ -1,95 +0,0 @@ -import { Message, MessageFlags, MessageComponentInteraction, ButtonInteraction, TextBasedChannel } from 'discord.js'; -import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10'; -import type { CommandMessage } from '../../../core/types/commands'; -import { hasManageGuildOrStaff } from '../../../core/lib/permissions'; -import logger from '../../../core/lib/logger'; -import type Amayo from '../../../core/client'; - -interface MobEditorState { - key: string; - name?: string; - category?: string; - stats?: any; // JSON libre, ej: { attack, hp, defense } - drops?: any; // JSON libre, tabla de recompensas -} - -export const command: CommandMessage = { - name: 'mob-crear', - type: 'message', - aliases: ['crear-mob','mobcreate'], - cooldown: 10, - description: 'Crea un Mob (enemigo) para este servidor con editor interactivo.', - category: 'Minijuegos', - usage: 'mob-crear ', - run: async (message: Message, args: string[], client: Amayo) => { - const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma); - if (!allowed) { await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.'); return; } - const key = args[0]?.trim(); - if (!key) { await message.reply('Uso: `!mob-crear `'); return; } - - const guildId = message.guild!.id; - const exists = await client.prisma.mob.findFirst({ where: { key, guildId } }); - if (exists) { await message.reply('❌ Ya existe un mob con esa key.'); return; } - - const state: MobEditorState = { key, stats: { attack: 5 }, drops: {} }; - - const channel = message.channel as TextBasedChannel & { send: Function }; - const editorMsg = await channel.send({ - content: `👾 Editor de Mob: \`${key}\``, - components: [ { type: 1, components: [ - { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'mb_base' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Stats (JSON)', custom_id: 'mb_stats' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Drops (JSON)', custom_id: 'mb_drops' }, - { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'mb_save' }, - { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'mb_cancel' }, - ] } ], - }); - - const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i)=> i.user.id === message.author.id }); - collector.on('collect', async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - if (i.customId === 'mb_cancel') { await i.deferUpdate(); await editorMsg.edit({ content: '❌ Editor cancelado.', components: [] }); collector.stop('cancel'); return; } - if (i.customId === 'mb_base') { await showBaseModal(i as ButtonInteraction, state); return; } - if (i.customId === 'mb_stats') { await showJsonModal(i as ButtonInteraction, state, 'stats', 'Stats del Mob (JSON)'); return; } - if (i.customId === 'mb_drops') { await showJsonModal(i as ButtonInteraction, state, 'drops', 'Drops del Mob (JSON)'); return; } - if (i.customId === 'mb_save') { - if (!state.name) { await i.reply({ content: '❌ Falta el nombre del mob.', flags: MessageFlags.Ephemeral }); return; } - await client.prisma.mob.create({ data: { guildId, key: state.key, name: state.name!, category: state.category ?? null, stats: state.stats ?? {}, drops: state.drops ?? {} } }); - await i.reply({ content: '✅ Mob guardado!', flags: MessageFlags.Ephemeral }); - await editorMsg.edit({ content: `✅ Mob \`${state.key}\` creado.`, components: [] }); - collector.stop('saved'); - return; - } - } catch (err) { - logger.error({err}, 'mob-crear'); - if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral }); - } - }); - collector.on('end', async (_c,r)=> { if (r==='time') { try { await editorMsg.edit({ content:'⏰ Editor expirado.', components: [] }); } catch {} } }); - }, -}; - -async function showBaseModal(i: ButtonInteraction, state: MobEditorState) { - const modal = { title: 'Configuración base del Mob', customId: 'mb_base_modal', components: [ - { type: ComponentType.Label, label: 'Nombre', component: { type: ComponentType.TextInput, customId: 'name', style: TextInputStyle.Short, required: true, value: state.name ?? '' } }, - { type: ComponentType.Label, label: 'Categoría', component: { type: ComponentType.TextInput, customId: 'cat', style: TextInputStyle.Short, required: false, value: state.category ?? '' } }, - ] } as const; - await i.showModal(modal); - try { const sub = await i.awaitModalSubmit({ time: 300_000 }); state.name = sub.components.getTextInputValue('name').trim(); state.category = sub.components.getTextInputValue('cat').trim() || undefined; await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral }); } catch {} -} - -async function showJsonModal(i: ButtonInteraction, state: MobEditorState, field: 'stats'|'drops', label: string) { - const current = JSON.stringify(state[field] ?? (field==='stats'? { attack: 5 }: {})); - const modal = { title: label, customId: `mb_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] = field==='stats' ? { attack: 5 } : {}; await sub.reply({ content: 'ℹ️ Limpio.', flags: MessageFlags.Ephemeral }); } - } catch {} -} diff --git a/src/.backup/mobEdit.ts.backup2 b/src/.backup/mobEdit.ts.backup2 deleted file mode 100644 index 68e81a4..0000000 --- a/src/.backup/mobEdit.ts.backup2 +++ /dev/null @@ -1,100 +0,0 @@ -import { Message, MessageFlags, MessageComponentInteraction, ButtonInteraction, TextBasedChannel } from 'discord.js'; -import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10'; -import type { CommandMessage } from '../../../core/types/commands'; -import { hasManageGuildOrStaff } from '../../../core/lib/permissions'; -import logger from '../../../core/lib/logger'; -import type Amayo from '../../../core/client'; - -interface MobEditorState { - key: string; - name?: string; - category?: string; - stats?: any; - drops?: any; -} - -export const command: CommandMessage = { - name: 'mob-editar', - type: 'message', - aliases: ['editar-mob','mobedit'], - cooldown: 10, - description: 'Edita un Mob (enemigo) de este servidor con editor interactivo.', - category: 'Minijuegos', - usage: 'mob-editar ', - run: async (message: Message, args: string[], client: Amayo) => { - const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma); - if (!allowed) { await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.'); return; } - const key = args[0]?.trim(); - if (!key) { await message.reply('Uso: `!mob-editar `'); return; } - const guildId = message.guild!.id; - - const mob = await client.prisma.mob.findFirst({ where: { key, guildId } }); - if (!mob) { await message.reply('❌ No existe un mob con esa key en este servidor.'); return; } - - const state: MobEditorState = { - key, - name: mob.name, - category: mob.category ?? undefined, - stats: mob.stats ?? {}, - drops: mob.drops ?? {}, - }; - - const channel = message.channel as TextBasedChannel & { send: Function }; - const editorMsg = await channel.send({ - content: `👾 Editor de Mob (editar): \`${key}\``, - components: [ { type: 1, components: [ - { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'mb_base' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Stats (JSON)', custom_id: 'mb_stats' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Drops (JSON)', custom_id: 'mb_drops' }, - { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'mb_save' }, - { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'mb_cancel' }, - ] } ], - }); - - const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i)=> i.user.id === message.author.id }); - collector.on('collect', async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - if (i.customId === 'mb_cancel') { await i.deferUpdate(); await editorMsg.edit({ content: '❌ Editor cancelado.', components: [] }); collector.stop('cancel'); return; } - if (i.customId === 'mb_base') { await showBaseModal(i as ButtonInteraction, state); return; } - if (i.customId === 'mb_stats') { await showJsonModal(i as ButtonInteraction, state, 'stats', 'Stats del Mob (JSON)'); return; } - if (i.customId === 'mb_drops') { await showJsonModal(i as ButtonInteraction, state, 'drops', 'Drops del Mob (JSON)'); return; } - if (i.customId === 'mb_save') { - if (!state.name) { await i.reply({ content: '❌ Falta el nombre del mob.', flags: MessageFlags.Ephemeral }); return; } - await client.prisma.mob.update({ where: { id: mob.id }, data: { name: state.name!, category: state.category ?? null, stats: state.stats ?? {}, drops: state.drops ?? {} } }); - await i.reply({ content: '✅ Mob actualizado!', flags: MessageFlags.Ephemeral }); - await editorMsg.edit({ content: `✅ Mob \`${state.key}\` actualizado.`, components: [] }); - collector.stop('saved'); - return; - } - } catch (err) { - logger.error({err}, 'mob-editar'); - if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral }); - } - }); - collector.on('end', async (_c,r)=> { if (r==='time') { try { await editorMsg.edit({ content:'⏰ Editor expirado.', components: [] }); } catch {} } }); - }, -}; - -async function showBaseModal(i: ButtonInteraction, state: MobEditorState) { - const modal = { title: 'Configuración base del Mob', customId: 'mb_base_modal', components: [ - { type: ComponentType.Label, label: 'Nombre', component: { type: ComponentType.TextInput, customId: 'name', style: TextInputStyle.Short, required: true, value: state.name ?? '' } }, - { type: ComponentType.Label, label: 'Categoría', component: { type: ComponentType.TextInput, customId: 'cat', style: TextInputStyle.Short, required: false, value: state.category ?? '' } }, - ] } as const; - await i.showModal(modal); - try { const sub = await i.awaitModalSubmit({ time: 300_000 }); state.name = sub.components.getTextInputValue('name').trim(); state.category = sub.components.getTextInputValue('cat').trim() || undefined; await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral }); } catch {} -} - -async function showJsonModal(i: ButtonInteraction, state: MobEditorState, field: 'stats'|'drops', label: string) { - const current = JSON.stringify(state[field] ?? (field==='stats'? { attack: 5 }: {})); - const modal = { title: label, customId: `mb_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] = field==='stats' ? { attack: 5 } : {}; await sub.reply({ content: 'ℹ️ Limpio.', flags: MessageFlags.Ephemeral }); } - } catch {} -} diff --git a/src/.backup/offerCreate.ts.backup2 b/src/.backup/offerCreate.ts.backup2 deleted file mode 100644 index 83b60a7..0000000 --- a/src/.backup/offerCreate.ts.backup2 +++ /dev/null @@ -1,129 +0,0 @@ -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 OfferState { - itemKey?: string; - enabled?: boolean; - price?: any; - startAt?: string; - endAt?: string; - perUserLimit?: number | null; - stock?: number | null; - metadata?: any; -} - -export const command: CommandMessage = { - name: 'offer-crear', - type: 'message', - aliases: ['crear-oferta','ofertacreate'], - cooldown: 10, - description: 'Crea una ShopOffer para este servidor con editor interactivo (price/ventanas/stock/limit).', - usage: 'offer-crear', - 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 guildId = message.guild!.id; - const state: OfferState = { enabled: true, price: {}, perUserLimit: null, stock: null, metadata: {} }; - - const editorMsg = await (message.channel as any).send({ - content: `🛒 Editor de Oferta (crear)`, - components: [ - { type: 1, components: [ - { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'of_base' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Precio (JSON)', custom_id: 'of_price' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'of_window' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Límites', custom_id: 'of_limits' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'of_meta' }, - ]}, - { type: 1, components: [ - { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'of_save' }, - { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'of_cancel' }, - ]}, - ], - }); - - const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i)=> i.user.id === message.author.id }); - collector.on('collect', async (i: MessageComponentInteraction) => { - try { - if (!i.isButton()) return; - switch (i.customId) { - case 'of_cancel': await i.deferUpdate(); await editorMsg.edit({ content: '❌ Editor de Oferta cancelado.', components: [] }); collector.stop('cancel'); return; - case 'of_base': await showBaseModal(i as ButtonInteraction, state); return; - case 'of_price': await showJsonModal(i as ButtonInteraction, state, 'price', 'Precio'); return; - case 'of_window': await showWindowModal(i as ButtonInteraction, state); return; - case 'of_limits': await showLimitsModal(i as ButtonInteraction, state); return; - case 'of_meta': await showJsonModal(i as ButtonInteraction, state, 'metadata', 'Meta'); return; - case 'of_save': - if (!state.itemKey) { await i.reply({ content: '❌ Falta itemKey en Base.', flags: MessageFlags.Ephemeral }); return; } - const item = await prisma.economyItem.findFirst({ where: { key: state.itemKey, OR: [{ guildId }, { guildId: null }] }, orderBy: [{ guildId: 'desc' }] }); - if (!item) { await i.reply({ content: '❌ Item no encontrado por key.', flags: MessageFlags.Ephemeral }); return; } - try { - await prisma.shopOffer.create({ - data: { - guildId, - itemId: item.id, - enabled: state.enabled ?? true, - price: state.price ?? {}, - startAt: state.startAt ? new Date(state.startAt) : null, - endAt: state.endAt ? new Date(state.endAt) : null, - perUserLimit: state.perUserLimit ?? null, - stock: state.stock ?? null, - metadata: state.metadata ?? {}, - } - }); - await i.reply({ content: '✅ Oferta guardada.', flags: MessageFlags.Ephemeral }); - await editorMsg.edit({ content: `✅ Oferta creada para ${state.itemKey}.`, components: [] }); - collector.stop('saved'); - } catch (err: any) { - await i.reply({ content: `❌ Error al guardar: ${err?.message ?? err}`, flags: MessageFlags.Ephemeral }); - } - return; - } - } catch (e) { - if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral }); - } - }); - collector.on('end', async (_c,r)=> { if (r==='time') { try { await editorMsg.edit({ content:'⏰ Editor expirado.', components: [] }); } catch {} } }); - } -}; - -async function showBaseModal(i: ButtonInteraction, state: OfferState) { - const modal = { title: 'Base de Oferta', customId: 'of_base_modal', components: [ - { type: ComponentType.Label, label: 'Item Key', component: { type: ComponentType.TextInput, customId: 'itemKey', style: TextInputStyle.Short, required: true, value: state.itemKey ?? '' } }, - { type: ComponentType.Label, label: 'Habilitada? (true/false)', component: { type: ComponentType.TextInput, customId: 'enabled', style: TextInputStyle.Short, required: false, value: String(state.enabled ?? true) } }, - ] } as const; - await i.showModal(modal); - try { const sub = await i.awaitModalSubmit({ time: 300_000 }); state.itemKey = sub.components.getTextInputValue('itemKey').trim(); const en = sub.components.getTextInputValue('enabled').trim(); state.enabled = en ? (en.toLowerCase() !== 'false') : (state.enabled ?? true); await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral }); } catch {} -} - -async function showJsonModal(i: ButtonInteraction, state: OfferState, field: 'price'|'metadata', title: string) { - const current = JSON.stringify(state[field] ?? {}); - const modal = { title, customId: `of_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 }); } } catch {} -} - -async function showWindowModal(i: ButtonInteraction, state: OfferState) { - const modal = { title: 'Ventana', customId: 'of_window_modal', components: [ - { type: ComponentType.Label, label: 'Inicio (ISO opcional)', component: { type: ComponentType.TextInput, customId: 'start', style: TextInputStyle.Short, required: false, value: state.startAt ?? '' } }, - { type: ComponentType.Label, label: 'Fin (ISO opcional)', component: { type: ComponentType.TextInput, customId: 'end', style: TextInputStyle.Short, required: false, value: state.endAt ?? '' } }, - ] } as const; - await i.showModal(modal); - try { const sub = await i.awaitModalSubmit({ time: 300_000 }); const s = sub.components.getTextInputValue('start').trim(); const e = sub.components.getTextInputValue('end').trim(); state.startAt = s || ''; state.endAt = e || ''; await sub.reply({ content: '✅ Ventana actualizada.', flags: MessageFlags.Ephemeral }); } catch {} -} - -async function showLimitsModal(i: ButtonInteraction, state: OfferState) { - const modal = { title: 'Límites', customId: 'of_limits_modal', components: [ - { type: ComponentType.Label, label: 'Límite por usuario (vacío = sin límite)', component: { type: ComponentType.TextInput, customId: 'limit', style: TextInputStyle.Short, required: false, value: state.perUserLimit != null ? String(state.perUserLimit) : '' } }, - { type: ComponentType.Label, label: 'Stock global (vacío = ilimitado)', component: { type: ComponentType.TextInput, customId: 'stock', style: TextInputStyle.Short, required: false, value: state.stock != null ? String(state.stock) : '' } }, - ] } as const; - await i.showModal(modal); - try { const sub = await i.awaitModalSubmit({ time: 300_000 }); const lim = sub.components.getTextInputValue('limit').trim(); const st = sub.components.getTextInputValue('stock').trim(); state.perUserLimit = lim ? Math.max(0, parseInt(lim,10)||0) : null; state.stock = st ? Math.max(0, parseInt(st,10)||0) : null; await sub.reply({ content: '✅ Límites actualizados.', flags: MessageFlags.Ephemeral }); } catch {} -} diff --git a/src/.backup/offerEdit.ts.backup2 b/src/.backup/offerEdit.ts.backup2 deleted file mode 100644 index 8f04f12..0000000 --- a/src/.backup/offerEdit.ts.backup2 +++ /dev/null @@ -1,148 +0,0 @@ -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 OfferState { - offerId: string; - itemKey?: string; - enabled?: boolean; - price?: any; - startAt?: string; - endAt?: string; - perUserLimit?: number | null; - stock?: number | null; - metadata?: any; -} - -export const command: CommandMessage = { - name: 'offer-editar', - type: 'message', - aliases: ['editar-oferta','offeredit'], - cooldown: 10, - description: 'Edita una ShopOffer por ID con editor interactivo (price/ventanas/stock/limit).', - usage: 'offer-editar ', - 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 offerId = args[0]?.trim(); - if (!offerId) { await message.reply('Uso: `!offer-editar `'); return; } - - const guildId = message.guild!.id; - const offer = await prisma.shopOffer.findUnique({ where: { id: offerId } }); - if (!offer || offer.guildId !== guildId) { await message.reply('❌ Oferta no encontrada para este servidor.'); return; } - - const item = await prisma.economyItem.findUnique({ where: { id: offer.itemId } }); - - const state: OfferState = { - offerId, - itemKey: item?.key, - enabled: offer.enabled, - price: offer.price ?? {}, - startAt: offer.startAt ? new Date(offer.startAt).toISOString() : '', - endAt: offer.endAt ? new Date(offer.endAt).toISOString() : '', - perUserLimit: offer.perUserLimit ?? null, - stock: offer.stock ?? null, - metadata: offer.metadata ?? {}, - }; - - const editorMsg = await (message.channel as any).send({ - content: `🛒 Editor de Oferta (editar): ${offerId}`, - components: [ - { type: 1, components: [ - { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'of_base' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Precio (JSON)', custom_id: 'of_price' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'of_window' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Límites', custom_id: 'of_limits' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'of_meta' }, - ] }, - { type: 1, components: [ - { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'of_save' }, - { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'of_cancel' }, - ] }, - ], - }); - - 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 'of_cancel': await i.deferUpdate(); await editorMsg.edit({ content: '❌ Editor de Oferta cancelado.', components: [] }); collector.stop('cancel'); return; - case 'of_base': await showBaseModal(i as ButtonInteraction, state); return; - case 'of_price': await showJsonModal(i as ButtonInteraction, state, 'price', 'Precio'); return; - case 'of_window': await showWindowModal(i as ButtonInteraction, state); return; - case 'of_limits': await showLimitsModal(i as ButtonInteraction, state); return; - case 'of_meta': await showJsonModal(i as ButtonInteraction, state, 'metadata', 'Meta'); return; - case 'of_save': - if (!state.itemKey) { await i.reply({ content: '❌ Falta itemKey en Base.', flags: MessageFlags.Ephemeral }); return; } - const it = await prisma.economyItem.findFirst({ where: { key: state.itemKey, OR: [{ guildId }, { guildId: null }] }, orderBy: [{ guildId: 'desc' }] }); - if (!it) { await i.reply({ content: '❌ Item no encontrado por key.', flags: MessageFlags.Ephemeral }); return; } - try { - await prisma.shopOffer.update({ - where: { id: state.offerId }, - data: { - itemId: it.id, - enabled: state.enabled ?? true, - price: state.price ?? {}, - startAt: state.startAt ? new Date(state.startAt) : null, - endAt: state.endAt ? new Date(state.endAt) : null, - perUserLimit: state.perUserLimit ?? null, - stock: state.stock ?? null, - metadata: state.metadata ?? {}, - } - }); - await i.reply({ content: '✅ Oferta actualizada.', flags: MessageFlags.Ephemeral }); - await editorMsg.edit({ content: `✅ Oferta ${state.offerId} actualizada.`, components: [] }); - collector.stop('saved'); - } catch (err: any) { - await i.reply({ content: `❌ Error al guardar: ${err?.message ?? err}`, flags: MessageFlags.Ephemeral }); - } - return; - } - } catch (e) { - 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 {} } }); - } -}; - -async function showBaseModal(i: ButtonInteraction, state: OfferState) { - const modal = { title: 'Base de Oferta', customId: 'of_base_modal', components: [ - { type: ComponentType.Label, label: 'Item Key', component: { type: ComponentType.TextInput, customId: 'itemKey', style: TextInputStyle.Short, required: true, value: state.itemKey ?? '' } }, - { type: ComponentType.Label, label: 'Habilitada? (true/false)', component: { type: ComponentType.TextInput, customId: 'enabled', style: TextInputStyle.Short, required: false, value: String(state.enabled ?? true) } }, - ] } as const; - await i.showModal(modal); - try { const sub = await i.awaitModalSubmit({ time: 300_000 }); state.itemKey = sub.components.getTextInputValue('itemKey').trim(); const en = sub.components.getTextInputValue('enabled').trim(); state.enabled = en ? (en.toLowerCase() !== 'false') : (state.enabled ?? true); await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral }); } catch {} -} - -async function showJsonModal(i: ButtonInteraction, state: OfferState, field: 'price'|'metadata', title: string) { - const current = JSON.stringify(state[field] ?? {}); - const modal = { title, customId: `of_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 }); } } catch {} -} - -async function showWindowModal(i: ButtonInteraction, state: OfferState) { - const modal = { title: 'Ventana', customId: 'of_window_modal', components: [ - { type: ComponentType.Label, label: 'Inicio (ISO opcional)', component: { type: ComponentType.TextInput, customId: 'start', style: TextInputStyle.Short, required: false, value: state.startAt ?? '' } }, - { type: ComponentType.Label, label: 'Fin (ISO opcional)', component: { type: ComponentType.TextInput, customId: 'end', style: TextInputStyle.Short, required: false, value: state.endAt ?? '' } }, - ] } as const; - await i.showModal(modal); - try { const sub = await i.awaitModalSubmit({ time: 300_000 }); const s = sub.components.getTextInputValue('start').trim(); const e = sub.components.getTextInputValue('end').trim(); state.startAt = s || ''; state.endAt = e || ''; await sub.reply({ content: '✅ Ventana actualizada.', flags: MessageFlags.Ephemeral }); } catch {} -} - -async function showLimitsModal(i: ButtonInteraction, state: OfferState) { - const modal = { title: 'Límites', customId: 'of_limits_modal', components: [ - { type: ComponentType.Label, label: 'Límite por usuario (vacío = sin límite)', component: { type: ComponentType.TextInput, customId: 'limit', style: TextInputStyle.Short, required: false, value: state.perUserLimit != null ? String(state.perUserLimit) : '' } }, - { type: ComponentType.Label, label: 'Stock global (vacío = ilimitado)', component: { type: ComponentType.TextInput, customId: 'stock', style: TextInputStyle.Short, required: false, value: state.stock != null ? String(state.stock) : '' } }, - ] } as const; - await i.showModal(modal); - try { const sub = await i.awaitModalSubmit({ time: 300_000 }); const lim = sub.components.getTextInputValue('limit').trim(); const st = sub.components.getTextInputValue('stock').trim(); state.perUserLimit = lim ? Math.max(0, parseInt(lim,10)||0) : null; state.stock = st ? Math.max(0, parseInt(st,10)||0) : null; await sub.reply({ content: '✅ Límites actualizados.', flags: MessageFlags.Ephemeral }); } catch {} -} diff --git a/src/.backup/player.ts.backup b/src/.backup/player.ts.backup deleted file mode 100644 index 1b7f972..0000000 --- a/src/.backup/player.ts.backup +++ /dev/null @@ -1,215 +0,0 @@ -import type { CommandMessage } from '../../../core/types/commands'; -import type Amayo from '../../../core/client'; -import { prisma } from '../../../core/database/prisma'; -import { getOrCreateWallet } from '../../../game/economy/service'; -import { getEquipment, getEffectiveStats } from '../../../game/combat/equipmentService'; -import type { ItemProps } from '../../../game/economy/types'; - -function parseItemProps(json: unknown): ItemProps { - if (!json || typeof json !== 'object') return {}; - return json as ItemProps; -} - -function fmtTool(props: ItemProps) { - const t = props.tool; - if (!t) return ''; - const icon = t.type === 'pickaxe' ? '⛏️' : t.type === 'rod' ? '🎣' : t.type === 'sword' ? '🗡️' : t.type === 'bow' ? '🏹' : t.type === 'halberd' ? '⚔️' : t.type === 'net' ? '🕸️' : '🔧'; - const tier = t.tier != null ? ` T${t.tier}` : ''; - return `${icon}${tier}`; -} - -export const command: CommandMessage = { - name: 'player', - type: 'message', - aliases: ['perfil', 'profile', 'yo', 'me'], - cooldown: 5, - description: 'Muestra toda tu información de jugador: stats, equipo, progreso y últimas actividades.', - usage: 'player [@usuario]', - run: async (message, args, _client: Amayo) => { - // Permitir ver perfil de otros usuarios mencionándolos - const targetUser = message.mentions.users.first() || message.author; - const userId = targetUser.id; - const guildId = message.guild!.id; - - // Obtener datos del jugador - const wallet = await getOrCreateWallet(userId, guildId); - const { eq, weapon, armor, cape } = await getEquipment(userId, guildId); - const stats = await getEffectiveStats(userId, guildId); - const playerState = await prisma.playerState.findUnique({ where: { userId_guildId: { userId, guildId } } }); - - // Progreso por áreas - const progress = await prisma.playerProgress.findMany({ - where: { userId, guildId }, - include: { area: true }, - orderBy: { updatedAt: 'desc' }, - take: 5, - }); - - // Últimas actividades de minijuegos - const recentRuns = await prisma.minigameRun.findMany({ - where: { userId, guildId }, - include: { area: true }, - orderBy: { startedAt: 'desc' }, - take: 5, - }); - - // Conteo de items en inventario - const inventoryCount = await prisma.inventoryEntry.count({ - where: { userId, guildId, quantity: { gt: 0 } }, - }); - - // Total de items (cantidad sumada) - const inventorySum = await prisma.inventoryEntry.aggregate({ - where: { userId, guildId }, - _sum: { quantity: true }, - }); - - // Compras totales - const purchaseCount = await prisma.shopPurchase.count({ - where: { userId, guildId }, - }); - - // Cooldowns activos - const activeCooldowns = await prisma.actionCooldown.findMany({ - where: { userId, guildId, until: { gt: new Date() } }, - orderBy: { until: 'asc' }, - take: 5, - }); - - // Construir el mensaje - const lines: string[] = []; - - // Header - lines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); - lines.push(`👤 **Perfil de ${targetUser.username}**`); - lines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`); - - // 📊 Stats Básicos - lines.push(`📊 **ESTADÍSTICAS**`); - lines.push(`❤️ HP: ${stats.hp}/${stats.maxHp}`); - lines.push(`⚔️ ATK: ${stats.damage}`); - lines.push(`🛡️ DEF: ${stats.defense}`); - lines.push(`💰 Monedas: ${wallet.coins}`); - lines.push(``); - - // 🎒 Inventario - lines.push(`🎒 **INVENTARIO**`); - lines.push(`📦 Items únicos: ${inventoryCount}`); - lines.push(`🔢 Total items: ${inventorySum._sum.quantity ?? 0}`); - lines.push(`🛒 Compras totales: ${purchaseCount}`); - lines.push(``); - - // 🧰 Equipamiento - lines.push(`🧰 **EQUIPAMIENTO**`); - if (weapon) { - const wProps = parseItemProps(weapon.props); - const wTool = fmtTool(wProps); - const wDmg = wProps.damage ? ` +${wProps.damage} ATK` : ''; - lines.push(`🗡️ Arma: ${weapon.name || weapon.key}${wTool ? ` ${wTool}` : ''}${wDmg}`); - } else { - lines.push(`🗡️ Arma: -`); - } - - if (armor) { - const aProps = parseItemProps(armor.props); - const aDef = aProps.defense ? ` +${aProps.defense} DEF` : ''; - lines.push(`🛡️ Armadura: ${armor.name || armor.key}${aDef}`); - } else { - lines.push(`🛡️ Armadura: -`); - } - - if (cape) { - const cProps = parseItemProps(cape.props); - const cHp = cProps.maxHpBonus ? ` +${cProps.maxHpBonus} HP` : ''; - lines.push(`🧥 Capa: ${cape.name || cape.key}${cHp}`); - } else { - lines.push(`🧥 Capa: -`); - } - lines.push(``); - - // 🗺️ Progreso por Áreas - if (progress.length > 0) { - lines.push(`🗺️ **PROGRESO EN ÁREAS**`); - for (const p of progress) { - const areaIcon = p.area.type === 'MINE' ? '⛏️' : p.area.type === 'LAGOON' ? '🎣' : p.area.type === 'FIGHT' ? '⚔️' : p.area.type === 'FARM' ? '🌾' : '🗺️'; - lines.push(`${areaIcon} ${p.area.name}: Nivel ${p.highestLevel}`); - } - lines.push(``); - } - - // 📜 Últimas Actividades - if (recentRuns.length > 0) { - lines.push(`📜 **ÚLTIMAS ACTIVIDADES**`); - for (const run of recentRuns.slice(0, 3)) { - const result = run.result as any; - const areaIcon = run.area.type === 'MINE' ? '⛏️' : run.area.type === 'LAGOON' ? '🎣' : run.area.type === 'FIGHT' ? '⚔️' : run.area.type === 'FARM' ? '🌾' : '🗺️'; - const timestamp = run.startedAt; - const relativeTime = getRelativeTime(timestamp); - const rewardsCount = result.rewards?.length ?? 0; - lines.push(`${areaIcon} ${run.area.name} (Nv.${run.level}) - ${rewardsCount} recompensas - ${relativeTime}`); - } - lines.push(``); - } - - // ⏱️ Cooldowns Activos - if (activeCooldowns.length > 0) { - lines.push(`⏱️ **COOLDOWNS ACTIVOS**`); - for (const cd of activeCooldowns) { - const remaining = Math.max(0, Math.ceil((cd.until.getTime() - Date.now()) / 1000)); - const cdName = formatCooldownKey(cd.key); - lines.push(`⏳ ${cdName}: ${formatDuration(remaining)}`); - } - lines.push(``); - } - - // Stats adicionales del PlayerState - if (playerState?.stats) { - const additionalStats = playerState.stats as any; - if (Object.keys(additionalStats).length > 0) { - lines.push(`🎯 **STATS ADICIONALES**`); - if (additionalStats.attack != null) lines.push(`⚔️ Ataque Base: ${additionalStats.attack}`); - if (additionalStats.defense != null) lines.push(`🛡️ Defensa Base: ${additionalStats.defense}`); - if (additionalStats.strength != null) lines.push(`💪 Fuerza: ${additionalStats.strength}`); - if (additionalStats.luck != null) lines.push(`🍀 Suerte: ${additionalStats.luck}`); - lines.push(``); - } - } - - // Footer - lines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); - lines.push(`Usa \`!inventario\` para ver tus items completos`); - - await message.reply(lines.join('\n')); - }, -}; - -// Helpers -function getRelativeTime(date: Date): string { - const seconds = Math.floor((Date.now() - date.getTime()) / 1000); - if (seconds < 60) return 'Hace un momento'; - if (seconds < 3600) return `Hace ${Math.floor(seconds / 60)}m`; - if (seconds < 86400) return `Hace ${Math.floor(seconds / 3600)}h`; - if (seconds < 604800) return `Hace ${Math.floor(seconds / 86400)}d`; - return date.toLocaleDateString(); -} - -function formatDuration(seconds: number): string { - if (seconds < 60) return `${seconds}s`; - if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; - const hours = Math.floor(seconds / 3600); - const mins = Math.floor((seconds % 3600) / 60); - return `${hours}h ${mins}m`; -} - -function formatCooldownKey(key: string): string { - // Convertir keys como "minigame:mine.starter" a "Mina" - if (key.startsWith('minigame:')) { - const areaKey = key.split(':')[1]; - if (areaKey?.includes('mine')) return 'Mina'; - if (areaKey?.includes('lagoon')) return 'Laguna'; - if (areaKey?.includes('fight')) return 'Pelea'; - if (areaKey?.includes('farm')) return 'Granja'; - return areaKey || key; - } - return key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); -} diff --git a/src/.backup/prompts/discord-api-expert.prompt.md b/src/.backup/prompts/discord-api-expert.prompt.md deleted file mode 100644 index 306b0fc..0000000 --- a/src/.backup/prompts/discord-api-expert.prompt.md +++ /dev/null @@ -1,15 +0,0 @@ -# Prompt: Discord.js + Discord API Expert Mode - -Whenever assisting with Discord.js or the Discord API: - -- Always prioritize information from: - - https://discordjs.guide - - https://github.com/discordjs/discord.js - - https://github.com/discordjs/discord-api-types - - https://discord.com/developers/docs/intro -- If documentation is incomplete, check the installed dependency in `node_modules`. -- Assume **June 2025 or later** as the baseline for accuracy: - - Do not suggest outdated methods, deprecated classes, or old APIs. - - Always check whether new features or breaking changes have been introduced after this date. -- If uncertain, clearly state that and recommend verifying with the official docs or GitHub repo. -- Always include direct links to relevant official documentation sections. diff --git a/src/.backup/prompts/discord-helper.prompt.md b/src/.backup/prompts/discord-helper.prompt.md deleted file mode 100644 index 195e3f9..0000000 --- a/src/.backup/prompts/discord-helper.prompt.md +++ /dev/null @@ -1,14 +0,0 @@ -# Prompt: Discord.js Expert Mode (Post-June 2025) - -Whenever assisting with Discord.js or related dependencies: - -- Always check official resources: - - https://discordjs.guide - - https://github.com/discordjs/discord.js - - https://github.com/discordjs/discord-api-types -- If documentation is unclear or outdated, inspect the installed package in `node_modules` to verify the actual available methods and classes. -- Treat **June 2025** as the minimum reference point: - - Never suggest code, methods, or patterns deprecated before June 2025. - - Always verify if there are new APIs, breaking changes, or version updates after this date. -- Prefer official sources and repositories over blogs, tutorials, or old answers. -- Always mention if information may be outdated and link to the package’s GitHub or changelog for verification. diff --git a/src/.backup/prompts/resource-checker.prompt.md b/src/.backup/prompts/resource-checker.prompt.md deleted file mode 100644 index 25d2d27..0000000 --- a/src/.backup/prompts/resource-checker.prompt.md +++ /dev/null @@ -1,41 +0,0 @@ -# Prompt: Node.js Resource Checker (Memory & CPU) - -Whenever Copilot generates, modifies, or reviews code in Node.js: - -1. **Memory Monitoring** - - Always remind to check memory usage with: - ```js - logger.info(process.memoryUsage()); - ``` - which returns RSS, Heap Used, Heap Total, External, and Array Buffers. - - If `Heap Used` grows continuously without freeing, suggest potential memory leaks (unreleased timers, listeners, or large cached data). - - If `RSS` (Resident Set Size) is unusually higher than `Heap Total`, warn that external/native modules might be consuming memory. - -2. **CPU Usage** - - Remind to check CPU load using: - ```bash - top -p - ``` - or Node.js profiling (`--inspect`, `clinic flame`). - - Flag infinite loops, synchronous blocking calls, or heavy computations running on the main thread as potential CPU overuse. - - Encourage offloading heavy tasks to Workers, child processes, or queues when appropriate. - -3. **Best Practices** - - Always validate memory and CPU usage after implementing new features. - - Suggest garbage collection checks (`global.gc()`) in development when Node is run with `--expose-gc`. - - Warn if long-living references (e.g., large Maps, Sets, caches, or listeners) might not be released. - - Highlight that CPU and memory efficiency are as important as correctness. - -4. **Output Analysis** - - When given memory stats like: - ``` - RSS: 186.9MB - Heap Used: 67.6MB - Heap Total: 71.2MB - External: 5.0MB - ``` - - Compare `Heap Used` vs `Heap Total`: if close to the limit, risk of OOM. - - Compare `RSS` vs `Heap Total`: if RSS is much larger, check for native module or buffer leaks. - - If growth is unbounded, warn about potential memory leaks. - -5. **Always remind** to rerun tests with `npx tsc --noEmit` (for type safety) and memory checks together, ensuring both correctness and performance. diff --git a/src/.backup/prompts/ts-validation.prompt.md b/src/.backup/prompts/ts-validation.prompt.md deleted file mode 100644 index 502a8a2..0000000 --- a/src/.backup/prompts/ts-validation.prompt.md +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: TypeScript Validation Mode - -Whenever Copilot generates or modifies TypeScript code: - -- Immediately remind the user to validate changes by running: - ```bash - npx tsc --noEmit diff --git a/src/.backup/views.backup/index.ejs b/src/.backup/views.backup/index.ejs deleted file mode 100644 index dc1f1a6..0000000 --- a/src/.backup/views.backup/index.ejs +++ /dev/null @@ -1,108 +0,0 @@ -<% 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 }); %> - -
-
- -
-
-
- - - - - - <%= appName %> • v<%= version %> - -
- -

- - Guía Completa - - - <%= appName %> - -

- -

- Sistema completo de economía, minijuegos, misiones y IA para Discord -

- -
- -
-
- - Comenzar ahora - - - - -
- - -
- -
- - Discord.js <%= djsVersion %> - - - <%= currentDateHuman %> - - - 23 Secciones • Creación de Contenido Incluida - -
-
-
-
- -
-
- -
- <%- include('partials/toc') %> - -
-
- <%- 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') %> -
-
-
-
-
diff --git a/src/.backup/views.backup/layouts/layout.ejs b/src/.backup/views.backup/layouts/layout.ejs deleted file mode 100644 index 297d1e0..0000000 --- a/src/.backup/views.backup/layouts/layout.ejs +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - <%= title || `${appName} | Guía Completa` %> - - - - - - - - - <% if (typeof head !== 'undefined' && head) { %> - <%= head %> - <% } %> - - - - <%- await include('../partials/navbar', { appName }) %> - - -
-
-
-
-
- -
- <%- body %> -
- -
-
-
-

<%= appName %>

-

Sistema completo de juego, economía y gestión para Discord

-
- -
- Primeros Pasos - - Comandos - - Minijuegos - - FAQ -
- -
-

- Versión <%= version %> • Discord.js <%= djsVersion %> • <%= currentDateHuman %> -

-

- Amayo © <%= new Date().getFullYear() %> — Documentación para usuarios finales de Discord -

-
- - -
-
- - - - <% if (typeof scripts !== 'undefined' && scripts) { %> - <%= scripts %> - <% } %> - - diff --git a/src/.backup/views.backup/pages/index.ejs b/src/.backup/views.backup/pages/index.ejs deleted file mode 100644 index 7b175b6..0000000 --- a/src/.backup/views.backup/pages/index.ejs +++ /dev/null @@ -1,99 +0,0 @@ -
-
- -
-
-
- - - - - - <%= appName %> • v<%= version %> - -
- -

- - Guía Completa - - - <%= appName %> - -

- -

- Sistema completo de economía, minijuegos, misiones y IA para Discord -

- - - -
- - Discord.js <%= djsVersion %> - - - <%= currentDateHuman %> - - - 23 Secciones • Creación de Contenido Incluida - -
-
-
-
- -
-
- -
- <%- await include('../partials/toc') %> - -
-
- <%- 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') %> -
-
- - -
-
-
diff --git a/src/.backup/views.backup/partials/navbar.ejs b/src/.backup/views.backup/partials/navbar.ejs deleted file mode 100644 index c86f8df..0000000 --- a/src/.backup/views.backup/partials/navbar.ejs +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/src/.backup/views.backup/partials/rightSidebar.ejs b/src/.backup/views.backup/partials/rightSidebar.ejs deleted file mode 100644 index 8d6b4ab..0000000 --- a/src/.backup/views.backup/partials/rightSidebar.ejs +++ /dev/null @@ -1,32 +0,0 @@ -
- -
-
-

Apoya el proyecto

-
-
- -
-
- - -
-

Novedades

-
    -
  • • Nueva guía con layout EJS
  • -
  • • Mejoras de rendimiento del bot
  • -
  • • Sistema de economía ampliado
  • -
-
- -
-

Recursos útiles

- -
-
diff --git a/src/.backup/views.backup/partials/sections/admin.ejs b/src/.backup/views.backup/partials/sections/admin.ejs deleted file mode 100644 index 5313c79..0000000 --- a/src/.backup/views.backup/partials/sections/admin.ejs +++ /dev/null @@ -1,4 +0,0 @@ -
-

⚙️ Panel de Administración

-

Contenido en migración a EJS…

-
diff --git a/src/.backup/views.backup/partials/sections/alianzas.ejs b/src/.backup/views.backup/partials/sections/alianzas.ejs deleted file mode 100644 index aa8ae37..0000000 --- a/src/.backup/views.backup/partials/sections/alianzas.ejs +++ /dev/null @@ -1,4 +0,0 @@ -
-

🤝 Sistema de Alianzas

-

Contenido en migración a EJS…

-
diff --git a/src/.backup/views.backup/partials/sections/cofres.ejs b/src/.backup/views.backup/partials/sections/cofres.ejs deleted file mode 100644 index 8843d86..0000000 --- a/src/.backup/views.backup/partials/sections/cofres.ejs +++ /dev/null @@ -1,29 +0,0 @@ -
-

🎁 Cofres y Recompensas

-

Abre cofres para conseguir recompensas aleatorias: monedas, items o incluso roles.

- -
-
-

🗝️ Abrir Cofres

-
- !abrir <itemKey> -
-

Ejemplo: !abrir daily_chest

-
-
-

⚙️ Definición de recompensas

-
-
{
-  "chest": {
-    "enabled": true,
-    "rewards": [
-      { "type": "coins", "amount": 500 },
-      { "type": "item", "itemKey": "health_potion", "qty": 3 }
-    ],
-    "consumeOnOpen": true
-  }
-}
-
-
-
-
diff --git a/src/.backup/views.backup/partials/sections/comandos-basicos.ejs b/src/.backup/views.backup/partials/sections/comandos-basicos.ejs deleted file mode 100644 index 04dd0ee..0000000 --- a/src/.backup/views.backup/partials/sections/comandos-basicos.ejs +++ /dev/null @@ -1,26 +0,0 @@ -
-

⚡ Comandos Básicos

-

- Estos son los comandos esenciales que necesitas conocer para empezar. -

- -
-
-

📋 Información y Utilidad

-
-
- !ayuda [comando|categoría] -

Muestra la lista de comandos. También puedes usar !help, !comandos o !cmds

-
-
- !ping -

Verifica la latencia del bot. También: !latency, !pong

-
-
- !player [@usuario] -

Muestra tu perfil completo de jugador con estadísticas, equipo e inventario. También: !perfil, !profile, !yo, !me

-
-
-
-
-
diff --git a/src/.backup/views.backup/partials/sections/configuracion.ejs b/src/.backup/views.backup/partials/sections/configuracion.ejs deleted file mode 100644 index aafb138..0000000 --- a/src/.backup/views.backup/partials/sections/configuracion.ejs +++ /dev/null @@ -1,4 +0,0 @@ -
-

🔧 Configuración del Servidor

-

Contenido en migración a EJS…

-
diff --git a/src/.backup/views.backup/partials/sections/consumibles.ejs b/src/.backup/views.backup/partials/sections/consumibles.ejs deleted file mode 100644 index be0ec94..0000000 --- a/src/.backup/views.backup/partials/sections/consumibles.ejs +++ /dev/null @@ -1,28 +0,0 @@ -
-

🍖 Consumibles y Pociones

-

Usa comida y pociones para curarte o ganar ventajas temporales.

- -
-
-

🍽️ Uso

-
- !comer <itemKey> -
-

Ejemplo: !comer minor_healing_potion

-
-
-

⚙️ Props JSON útiles

-
-
{
-  "food": { "healHp": 50, "healPercent": 0, "cooldownSeconds": 60 },
-  "stackable": true, "maxInventory": 10
-}
-
-
-
- -
- Cooldowns -

Algunos consumibles comparten cooldown por categoría. Usa !cooldowns para revisarlos.

-
-
diff --git a/src/.backup/views.backup/partials/sections/crafteo.ejs b/src/.backup/views.backup/partials/sections/crafteo.ejs deleted file mode 100644 index de3a2e0..0000000 --- a/src/.backup/views.backup/partials/sections/crafteo.ejs +++ /dev/null @@ -1,77 +0,0 @@ -
-

🛠️ Crafteo

-

Combina materiales para crear objetos. Algunas recetas requieren nivel mínimo o herramientas específicas.

- -
-
-

📜 Ver Recetas

-
- !recetas -
-

Lista de recetas disponibles y sus requisitos.

-
-
-

🧪 Crear un Objeto

-
- !craftear <receta> [cantidad] -
-

Ejemplo: !craftear espada 1

-
-
- -
-
-

🧭 Cómo craftear (paso a paso)

-
    -
  1. Escribe !recetas y elige el nombre clave de la receta (por ejemplo: espada).
  2. -
  3. Revisa los ingredientes requeridos y tu inventario con !inventario.
  4. -
  5. Si la receta exige nivel/herramienta, verifica tu estado: !perfil y tu equipo con !equipo.
  6. -
  7. Ejecuta !craftear <receta> [cantidad]. Ejemplos: -
    -
    !craftear espada
    -
    !craftear espada 3
    -
    -
  8. -
  9. Si cumples los requisitos, recibirás el/los objeto(s) al instante en tu inventario.
  10. -
-

Consejo: si una receta admite múltiples resultados o variantes, en !recetas verás notas adicionales.

-
- -
-

📌 Requisitos típicos

-
    -
  • Ingredientes exactos en cantidad suficiente.
  • -
  • Nivel mínimo del jugador para la receta.
  • -
  • Herramienta adecuada equipada (si aplica).
  • -
-
- Algunas recetas avanzadas podrían requerir materiales raros o pasos previos (p. ej., procesar un material en fundición antes de craftear). -
-
-
- -
-

✅ Mensajes esperados

-
    -
  • Éxito: “Has crafteado <objeto> x<cantidad>”.
  • -
  • Faltan materiales: “No tienes suficientes <material>”.
  • -
  • Requisito: “Nivel insuficiente para esta receta” o “Necesitas <herramienta> equipada”.
  • -
-
- -
- Nota -

Las recetas pueden actualizarse con nuevos parches. Revisa !recetas después de una actualización.

-
- -
- Errores comunes y cómo resolverlos -
    -
  • Receta no encontrada: Asegúrate de usar el nombre correcto tal como aparece en !recetas.
  • -
  • Faltan materiales: Junta los recursos con minijuegos o compra/intercambia y vuelve a intentar.
  • -
  • Nivel insuficiente: Sube de nivel con actividades del juego hasta cumplir el requisito.
  • -
  • Herramienta incorrecta o sin durabilidad: Equipa la herramienta adecuada o repárala/sustitúyela.
  • -
  • Cantidad demasiado alta: Reduce la cantidad o craftea en varios intentos.
  • -
-
-
diff --git a/src/.backup/views.backup/partials/sections/creacion-contenido.ejs b/src/.backup/views.backup/partials/sections/creacion-contenido.ejs deleted file mode 100644 index 9788718..0000000 --- a/src/.backup/views.backup/partials/sections/creacion-contenido.ejs +++ /dev/null @@ -1,156 +0,0 @@ -
-

🎨 Creación de Contenido

-

Guía técnica paso a paso para crear items, áreas/niveles, mobs, ofertas y ahora componentes RPG avanzados (durabilidad por instancia, efectos de estado, penalizaciones de muerte, rachas, riskFactor de áreas). Requiere permiso Manage Guild o rol de staff.

- -
-
-

📦 Items: crear/editar

-
-
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
-
-

Usa !item-editar, !item-ver, !items-lista. Para herramientas NO apilables la durabilidad se gestiona por instancias dentro de inventory.state.instances.

-
-
Novedades RPG (Items)
-
    -
  • Durabilidad por instancia: si stackable=false y breakable.enabled=true, cada unidad es una instancia con su propia durabilidad.
  • -
  • Mutaciones / Encantamientos: se reflejan sumando bonuses (damageBonus, defenseBonus, maxHpBonus).
  • -
  • Ítem purga efectos: puedes crear tu propia poción local: { "usable": true, "purgeAllEffects": true } y usarla con !efectos purgar.
  • -
-
-
-
-

🧭 Áreas y Niveles (MINE/LAGOON/FIGHT/FARM)

-
-
1) Crear área
-!area-crear mine.iron_cavern
-Config (JSON): {
-  "cooldownSeconds": 60,
-  "description": "Caverna rica en hierro",
-  "icon": "⛏️",
-  "riskFactor": 2
-}
-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
-
-
-
Errores comunes
-
    -
  • mobKey o itemKey inexistente → crea primero o corrige la key
  • -
  • Pesos mal balanceados → revisa weight (no negativos; no tienen que sumar 100)
  • -
  • Herramienta requerida mal configurada → revisa toolType y minTier
  • -
  • riskFactor (0-3) afecta % de oro que pierdes al morir (escala hasta 25% total con nivel).
  • -
-
-
-
- -
-
-

👹 Mobs (enemigos/NPCs)

-
-
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
-
-

Revisa con !mobs-lista y !mob-eliminar <key> si necesitas limpiar datos de prueba.

-
-
Integración combate
-
    -
  • El daño del jugador usa arma + mutaciones + racha (1% cada 3 victorias, cap 30%).
  • -
  • Defensa reduce daño hasta 60% (5% por punto, cap).
  • -
  • Si daño efectivo = 0 → derrota automática (flag autoDefeatNoWeapon).
  • -
-
-
-
-

🛒 Ofertas de Tienda

-
-
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
-
-
-
Errores comunes
-
    -
  • itemKey no existe → crea el ítem primero
  • -
  • Formato de precio inválido → respeta estructura de coins e items
  • -
  • Ventana inválida → usa fechas ISO: YYYY-MM-DDTHH:MM:SSZ
  • -
  • Para vender una poción de purga local crea un ítem consumible y ofrece en la tienda.
  • -
-
-
-
- -
- Recomendaciones -
    -
  • Usa keys en minúsculas y sin espacios (únicas por servidor).
  • -
  • Guarda plantillas de Props JSON para acelerar creación.
  • -
  • Prueba tras crear: !craftear, !abrir, !equipar, !efectos, !deathlog.
  • -
  • Si ajustas valores de riesgo o nivel alto prueba la pérdida real (usa un alt) para validar balance.
  • -
  • Consulta auditoría de muertes: !deathlog para detectar abusos o mal balance.
  • -
-
-
Resumen rápido nuevas claves JSON
- area.config.riskFactor: 0-3 (aumenta % oro perdido) - item.props.breakable.maxDurability / durabilityPerUse - item.props.tool { type, tier } - item.props.purgeAllEffects = true (ítem purga) - status effects: almacenados en DB (PlayerStatusEffect) - death penalty: porcentaje dinámico + fatiga escalada -
-
-
diff --git a/src/.backup/views.backup/partials/sections/economia.ejs b/src/.backup/views.backup/partials/sections/economia.ejs deleted file mode 100644 index 7de731a..0000000 --- a/src/.backup/views.backup/partials/sections/economia.ejs +++ /dev/null @@ -1,31 +0,0 @@ -
-

💰 Sistema de Economía

-

Gana y gestiona monedas para comprar items, participar en eventos y mejorar tu progreso.

- -
-
-

🪙 Ver tus Monedas

-
-
- !monedas [@usuario] -
-

Muestra el balance de monedas tuyo o de otro usuario.

-
-
-
-

💸 Cómo Ganar Monedas

-
    -
  • Jugar minijuegos (minar, pescar, pelear, plantar)
  • -
  • Completar misiones
  • -
  • Mantener tu racha diaria
  • -
  • Abrir cofres
  • -
  • Vender items (si está habilitado)
  • -
-
-
- -
- ⚠️ Importante: -

Las monedas son específicas por servidor. Cada servidor de Discord tiene su propia economía independiente.

-
-
diff --git a/src/.backup/views.backup/partials/sections/ejemplos-avanzados.ejs b/src/.backup/views.backup/partials/sections/ejemplos-avanzados.ejs deleted file mode 100644 index 258a840..0000000 --- a/src/.backup/views.backup/partials/sections/ejemplos-avanzados.ejs +++ /dev/null @@ -1,124 +0,0 @@ -
-

🧪 Ejemplos Avanzados

-

Workflows completos inspirados en tu documentación para staff. Sigue los pasos y copia/pega los JSON cuando se soliciten.

- -
-
-

1) Sistema de Minería (básico)

-
# Í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}]}
-
- -
-

2) Cofre de Recompensa Diaria

-
!item-crear daily_chest
-Props (JSON): {
-  "chest": {"enabled": true, "rewards": [
-    {"type": "coins", "amount": 500},
-    {"type": "item", "itemKey": "health_potion", "qty": 3}
-  ], "consumeOnOpen": true}
-}
-
- -
-

3) Espada Legendaria (cadena resumida)

-
# 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
-
- -
-

4) Área Avanzada con Riesgo y Mobs

-
# Área con factor de riesgo (aumenta penalización oro al morir)
-!area-crear arena.blood_pit
-Config (JSON): {"cooldownSeconds": 90, "icon": "⚔️", "riskFactor": 3, "description": "La fosa sangrienta"}
-!area-nivel arena.blood_pit 1
-Requisitos (JSON): {"tool": {"required": true, "toolType": "sword", "minTier": 2}}
-Recompensas (JSON): {"draws": 2, "table": [
-  {"type": "coins", "amount": 120, "weight": 10},
-  {"type": "item",  "itemKey": "blood_shard", "qty": 1, "weight": 4}
-]}
-Mobs (JSON): {"draws": 2, "table": [
-  {"mobKey": "goblin", "weight": 8},
-  {"mobKey": "cave_spider", "weight": 5}
-]}
-
- -
-

5) Ítem Poción de Purga Local

-
!item-crear purge_potion
-Props (JSON): {"usable": true, "purgeAllEffects": true, "icon": "🧪"}
-# Se consume al usar: !efectos purgar
-# Para venderla: crear oferta o poner drop en cofre.
-
- -
-

6) Introducción a Status Effects Manuales

-
# (Opcional) Aplicar un efecto custom vía comando admin futuro
-# Ejemplo conceptual JSON (guardado server-side):
-{
-  "type": "BLESSING",
-  "magnitude": 0.10,   # +10% daño (interpretación futura)
-  "durationMs": 600000 # 10 min
-}
-# Los efectos actuales: FATIGUE (reduce daño y defensa)
-# Ver activos: !efectos
-
- -
-

7) Auditoría de Muertes

-
# Ver últimas muertes y penalizaciones
-!deathlog           # por defecto 10
-!deathlog 20        # máximo 20
-
-# Interpreta columnas
-# HH:MM:SS | areaKey Lnivel | -oro | % | Fatiga | sin arma?
-
-# Ajusta balance si ves pérdidas demasiado altas en cierto nivel/riskFactor.
-
- -
-

8) Cadena Completa: Creación → Riesgo → Muerte

-
# 1. Crear arma y área con riesgo
-!item-crear bone_sword
-Props (JSON): {"tool": {"type": "sword", "tier": 1}, "damage": 9, "breakable": {"enabled": true, "maxDurability": 80}}
-!area-crear arena.bone_trial
-Config (JSON): {"cooldownSeconds": 45, "riskFactor": 1, "icon": "🗡️"}
-!area-nivel arena.bone_trial 1
-Requisitos (JSON): {"tool": {"required": true, "toolType": "sword", "minTier": 1}}
-Mobs (JSON): {"draws":1, "table":[{"mobKey":"goblin","weight":10}]}
-
-# 2. Pelear varias veces para subir racha y ver bonus daño (!player)
-# 3. Morir intencionalmente con monedas => verifica !deathlog
-# 4. Aplicar purga de efectos si acumulaste FATIGUE
-!efectos purgar
-
-# Ajusta riskFactor o nivel si la penalización % es muy baja/alta.
-
-
-
diff --git a/src/.backup/views.backup/partials/sections/ejemplos-basicos.ejs b/src/.backup/views.backup/partials/sections/ejemplos-basicos.ejs deleted file mode 100644 index 8e8dc75..0000000 --- a/src/.backup/views.backup/partials/sections/ejemplos-basicos.ejs +++ /dev/null @@ -1,47 +0,0 @@ -
-

🧭 Ejemplos Básicos

-

Un arranque rápido con los comandos más usados. Copia y pega en tu servidor:

- -
-
-

👤 Perfil y progreso

-
-
!player
-!stats
-!logros
-!misiones
-!cooldowns
-
-
- -
-

🎮 Minijuegos

-
-
!mina
-!pescar
-!pelear
-!plantar
-
-

Tip: Puedes pasar nivel o herramienta, ej. !mina 2 iron_pickaxe

-
- -
-

🎒 Inventario y equipo

-
-
!inventario
-!equipar weapon iron_sword
-!comer minor_healing_potion
-
-
- -
-

💰 Economía

-
-
!monedas
-!tienda
-!comprar health_potion 2
-!craftear iron_sword
-
-
-
-
diff --git a/src/.backup/views.backup/partials/sections/encantamientos.ejs b/src/.backup/views.backup/partials/sections/encantamientos.ejs deleted file mode 100644 index c419f2d..0000000 --- a/src/.backup/views.backup/partials/sections/encantamientos.ejs +++ /dev/null @@ -1,26 +0,0 @@ -
-

✨ Encantamientos

-

Aplica mutaciones para mejorar armas, armaduras o herramientas según políticas por ítem.

- -
-
-

🪄 Usar encantamientos

-
- !encantar <itemKey> <mutationKey> -
-

Ejemplo: !encantar iron_sword ruby_core

-
-
-

⚙️ Política por item (Props)

-
-
{
-  "mutationPolicy": {
-    "allowedKeys": ["ruby_core", "emerald_core", "sharpness_enchant"],
-    "deniedKeys": ["curse_weakness"]
-  }
-}
-
-

Define llaves permitidas/prohibidas por ítem.

-
-
-
diff --git a/src/.backup/views.backup/partials/sections/estadisticas.ejs b/src/.backup/views.backup/partials/sections/estadisticas.ejs deleted file mode 100644 index e0b81a0..0000000 --- a/src/.backup/views.backup/partials/sections/estadisticas.ejs +++ /dev/null @@ -1,4 +0,0 @@ -
-

📊 Estadísticas y Progreso

-

Contenido en migración a EJS…

-
diff --git a/src/.backup/views.backup/partials/sections/faq.ejs b/src/.backup/views.backup/partials/sections/faq.ejs deleted file mode 100644 index 46ed53d..0000000 --- a/src/.backup/views.backup/partials/sections/faq.ejs +++ /dev/null @@ -1,67 +0,0 @@ -
-

❓ Preguntas Frecuentes

-
-
- ¿Puedo editar un item después de crearlo? -
- Sí, usa !item-editar <key>. Para ver detalles sin editar: !item-ver <key>. -
-
- -
- ¿Cómo elimino un item? -
- Usa !item-eliminar <key>. Atención: es permanente y no se puede deshacer. -
-
- -
- ¿Cómo veo todos los items creados? -
- !items-lista [página] muestra una lista paginada con botones para ver detalles. -
-
- -
- ¿Qué formato tienen las fechas ISO? -
- Usa YYYY-MM-DDTHH:MM:SSZ. Ejemplos: 2025-01-15T00:00:00Z, 2025-12-25T23:59:59Z. -
-
- -
- ¿Puedo crear items globales? -
- Solo los administradores del bot pueden crear items globales. Los items que crees serán locales a tu servidor. -
-
- -
- ¿Cuántos niveles puedo crear por área? -
- No hay límite técnico; se recomiendan 5–10 por área para una progresión balanceada. -
-
- -
- ¿Qué pasa si un jugador no tiene la herramienta requerida? -
- El bot indica la herramienta y el tier mínimo necesarios según los requisitos del nivel. -
-
- -
- ¿Cómo funcionan los pesos (weights)? -
- Son probabilidades relativas. Si A tiene peso 10 y B peso 5, A es el doble de probable (10/15 vs 5/15). -
-
- -
- ¿Puedo hacer que un ítem cure porcentaje de vida? -
- Sí, en props usa food.healPercent (ej. 50) y un cooldown con food.cooldownSeconds. -
-
-
-
diff --git a/src/.backup/views.backup/partials/sections/fundicion.ejs b/src/.backup/views.backup/partials/sections/fundicion.ejs deleted file mode 100644 index 0ae06d0..0000000 --- a/src/.backup/views.backup/partials/sections/fundicion.ejs +++ /dev/null @@ -1,24 +0,0 @@ -
-

🔥 Sistema de Fundición

-

Transforma materiales crudos en recursos refinados con tiempo de espera y reclamo.

- -
-
-

⏳ Flujo de uso

-
    -
  1. Inicia: !fundir (ingresa inputs y output)
  2. -
  3. Espera el tiempo indicado
  4. -
  5. Reclama: !fundir-reclamar
  6. -
-
-
-

🧪 Receta ejemplo

-
-
Input: copper_ore x5, coal x2
-Output: copper_ingot x2
-Duración: 300s
-
-

La configuración exacta la define el equipo en base de datos.

-
-
-
diff --git a/src/.backup/views.backup/partials/sections/ia.ejs b/src/.backup/views.backup/partials/sections/ia.ejs deleted file mode 100644 index 1105def..0000000 --- a/src/.backup/views.backup/partials/sections/ia.ejs +++ /dev/null @@ -1,4 +0,0 @@ -
-

🤖 Inteligencia Artificial

-

Contenido en migración a EJS…

-
diff --git a/src/.backup/views.backup/partials/sections/inventario-equipo.ejs b/src/.backup/views.backup/partials/sections/inventario-equipo.ejs deleted file mode 100644 index fe65f69..0000000 --- a/src/.backup/views.backup/partials/sections/inventario-equipo.ejs +++ /dev/null @@ -1,48 +0,0 @@ -
-

🎒 Inventario y Equipo

-

- Gestiona todos tus items y equipa armas, armaduras y capas para mejorar tus estadísticas. -

- -
-
-

📦 Ver tu Inventario

-
-
- !inventario [página|filtro] -

Aliases: !inv

-
-

Muestra todos tus items con cantidades, información de herramientas y estadísticas.

-
-
- -
-

🧤 Equipar Items

-
-
- !equipar <slot> <itemKey> -

Aliases: !equip

-
-
-
-

⚔️ weapon

-

Armas que aumentan tu daño (ATK)

-
-
-

🛡️ armor

-

Armaduras que aumentan tu defensa (DEF)

-
-
-

🧥 cape

-

Capas con bonos especiales (HP, stats)

-
-
-
-
-
- -
- 💡 Tip: -

Usa !player para ver rápidamente tu equipo actual y las estadísticas que te otorgan.

-
-
diff --git a/src/.backup/views.backup/partials/sections/logros.ejs b/src/.backup/views.backup/partials/sections/logros.ejs deleted file mode 100644 index 1e6a687..0000000 --- a/src/.backup/views.backup/partials/sections/logros.ejs +++ /dev/null @@ -1,28 +0,0 @@ -
-

🏆 Logros

-

Completa objetivos para obtener recompensas únicas y mostrar tu progreso.

- -
-
-

📊 Ver tus Logros

-
- !logros -
-

Muestra logros completados y pendientes, con su progreso.

-
-
-

🎯 Progreso

-
    -
  • Juega minijuegos y combates
  • -
  • Completa misiones
  • -
  • Alcanza rachas diarias
  • -
  • Explora nuevas funciones (crafteo, fundición, etc.)
  • -
-
-
- -
- Tip -

Algunos logros otorgan títulos o insignias visibles en el servidor. ¡Presúmelos!

-
-
diff --git a/src/.backup/views.backup/partials/sections/minijuegos.ejs b/src/.backup/views.backup/partials/sections/minijuegos.ejs deleted file mode 100644 index 650c953..0000000 --- a/src/.backup/views.backup/partials/sections/minijuegos.ejs +++ /dev/null @@ -1,57 +0,0 @@ -
-

🎯 Minijuegos y Actividades

-

- Los minijuegos son la forma principal de ganar recursos, monedas y experiencia. Cada uno tiene su propio estilo y recompensas. -

- -
-
-

⛏️ Minar (Mining)

-
-

Ve a la mina y extrae recursos minerales valiosos. Necesitas un pico para minar.

-
- !mina [nivel] [herramienta] [area:clave] -

Aliases: !minar

-
-
-
- -
-

🎣 Pescar (Fishing)

-
-

Lanza tu caña en la laguna y captura peces y tesoros acuáticos. Necesitas una caña de pescar.

-
- !pescar [nivel] [herramienta] [area:clave] -

Aliases: !fish

-
-
-
- -
-

⚔️ Pelear (Combat)

-
-

Entra a la arena y enfrenta enemigos peligrosos. Las armas mejoran tu daño.

-
- !pelear [nivel] [arma] [area:clave] -

Aliases: !fight, !arena

-
-
-
- -
-

🌾 Plantar/Cultivar (Farming)

-
-

Cultiva plantas y cosecha alimentos en tu granja. Usa una azada para mejores resultados.

-
- !plantar [nivel] [herramienta] -

Aliases: !farm

-
-
-
-
- -
- ⏰ Cooldowns: -

Cada minijuego tiene un tiempo de espera (cooldown) entre usos. Usa !cooldowns para ver tus tiempos activos.

-
-
diff --git a/src/.backup/views.backup/partials/sections/misiones.ejs b/src/.backup/views.backup/partials/sections/misiones.ejs deleted file mode 100644 index 8a1c28d..0000000 --- a/src/.backup/views.backup/partials/sections/misiones.ejs +++ /dev/null @@ -1,34 +0,0 @@ -
-

📝 Misiones

-

Tareas con objetivos y recompensas. Úsalas para guiar la progresión diaria y semanal.

- -
-
-

📋 Ver y Reclamar

-
    -
  • !misiones — Ver misiones disponibles
  • -
  • !mision-reclamar <key> — Reclamar recompensa
  • -
-
-
-

🛠️ Crear como Admin

-
-
-
!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 }
-
-

Edita desde Discord con botones y modales; no necesitas código.

-
-
-
- -
- Tipos y requisitos comunes -
    -
  • Tipos: daily, weekly, one_time, repeatable
  • -
  • Requisitos: mine_count, fish_count, fight_count, collect_items, defeat_mobs
  • -
-
-
diff --git a/src/.backup/views.backup/partials/sections/primeros-pasos.ejs b/src/.backup/views.backup/partials/sections/primeros-pasos.ejs deleted file mode 100644 index 39eb827..0000000 --- a/src/.backup/views.backup/partials/sections/primeros-pasos.ejs +++ /dev/null @@ -1,39 +0,0 @@ -
-

🚀 Primeros Pasos

-

- ¡Bienvenido a Amayo Bot! Este bot transforma tu servidor de Discord en una experiencia de juego completa con economía, minijuegos, misiones y mucho más. -

- -
-

✨ ¿Qué puedes hacer con Amayo?

-
    -
  • Jugar Minijuegos: Mina recursos, pesca, pelea contra enemigos y cultiva en granjas
  • -
  • Economía Completa: Gana monedas, compra en la tienda, craftea items y gestiona tu inventario
  • -
  • Sistema de Progresión: Sube de nivel, completa misiones, desbloquea logros y mantén tu racha diaria
  • -
  • Personalización: Equipa armas, armaduras y capas para mejorar tus estadísticas
  • -
  • IA Conversacional: Chatea con Gemini AI directamente desde Discord
  • -
  • Sistema de Alianzas: Comparte enlaces de invitación y gana puntos para tu servidor
  • -
-
- -
-
-

⚡ Prefix del Bot

-

- El prefix por defecto es ! -

-

- Los administradores pueden cambiarlo con !configuracion -

-
-
-

❓ Obtener Ayuda

-

- Usa !ayuda para ver todos los comandos disponibles -

-

- También puedes usar !ayuda <comando> para detalles específicos -

-
-
-
diff --git a/src/.backup/views.backup/partials/sections/racha.ejs b/src/.backup/views.backup/partials/sections/racha.ejs deleted file mode 100644 index 0f0d092..0000000 --- a/src/.backup/views.backup/partials/sections/racha.ejs +++ /dev/null @@ -1,22 +0,0 @@ -
-

🔥 Racha Diaria

-

Entra cada día y realiza acciones para mantener tu racha. A mayor racha, mejores recompensas.

- -
-
-

📆 Comandos útiles

-
    -
  • !racha — Ver tu racha actual
  • -
  • !cooldowns — Revisa tiempos de espera de minijuegos
  • -
-
-
-

🎁 Beneficios

-
    -
  • Bonos de monedas diarios o semanales
  • -
  • Acceso a cofres o misiones especiales
  • -
  • Progreso extra en logros
  • -
-
-
-
diff --git a/src/.backup/views.backup/partials/sections/recordatorios.ejs b/src/.backup/views.backup/partials/sections/recordatorios.ejs deleted file mode 100644 index 1c9010b..0000000 --- a/src/.backup/views.backup/partials/sections/recordatorios.ejs +++ /dev/null @@ -1,4 +0,0 @@ -
-

⏰ Sistema de Recordatorios

-

Contenido en migración a EJS…

-
diff --git a/src/.backup/views.backup/partials/sections/sistema-juego.ejs b/src/.backup/views.backup/partials/sections/sistema-juego.ejs deleted file mode 100644 index fa0155c..0000000 --- a/src/.backup/views.backup/partials/sections/sistema-juego.ejs +++ /dev/null @@ -1,36 +0,0 @@ -
-

🎮 Sistema de Juego

-

- El sistema de juego de Amayo incluye HP (puntos de vida), estadísticas de combate, niveles de progresión y más. -

- -
-
-

⚔️ Estadísticas de Combate

-
    -
  • HP (Vida): Tus puntos de vida actuales y máximos
  • -
  • ATK (Ataque): Daño que infliges a los enemigos
  • -
  • DEF (Defensa): Reduce el daño recibido
  • -
  • Bonos de Equipo: Las armas, armaduras y capas mejoran tus stats
  • -
-
-
-

📊 Ver tus Estadísticas

-
-
- !player -

Vista general de tu perfil

-
-
- !stats -

Estadísticas detalladas de todas tus actividades

-
-
-
-
- -
- 💡 Consejo: -

Equipa mejores armas y armaduras para aumentar tus estadísticas y tener más éxito en los minijuegos de combate.

-
-
diff --git a/src/.backup/views.backup/partials/sections/tienda.ejs b/src/.backup/views.backup/partials/sections/tienda.ejs deleted file mode 100644 index 0c83abf..0000000 --- a/src/.backup/views.backup/partials/sections/tienda.ejs +++ /dev/null @@ -1,28 +0,0 @@ -
-

🛒 Tienda

-

Compra y vende objetos utilizando tus monedas. La disponibilidad puede variar según el servidor.

- -
-
-

🧾 Ver Catálogo

-
-
- !tienda -
-

Muestra la lista de items disponibles actualmente.

-
-
-
-

🛍️ Comprar Items

-
- !comprar <item> [cantidad] -
-

Ejemplo: !comprar pocion 3

-
-
- -
- Consejo -

Algunos items tienen descuentos temporales o packs especiales. ¡Revisa la tienda con frecuencia!

-
-
diff --git a/src/.backup/views.backup/partials/sections/tips.ejs b/src/.backup/views.backup/partials/sections/tips.ejs deleted file mode 100644 index 2fef493..0000000 --- a/src/.backup/views.backup/partials/sections/tips.ejs +++ /dev/null @@ -1,4 +0,0 @@ -
-

💡 Tips y Trucos

-

Contenido en migración a EJS…

-
diff --git a/src/.backup/views.backup/partials/toc.ejs b/src/.backup/views.backup/partials/toc.ejs deleted file mode 100644 index 4d2da4e..0000000 --- a/src/.backup/views.backup/partials/toc.ejs +++ /dev/null @@ -1,32 +0,0 @@ - diff --git a/src/commands/messages/alliaces/createDisplayComponent.ts.backup b/src/commands/messages/alliaces/createDisplayComponent.ts.backup deleted file mode 100644 index 78f4716..0000000 --- a/src/commands/messages/alliaces/createDisplayComponent.ts.backup +++ /dev/null @@ -1,1381 +0,0 @@ -import { - ButtonInteraction, - Message, - MessageComponentInteraction, - MessageFlags, - ModalSubmitInteraction, - TextChannel, -} from "discord.js"; -import { - ComponentType, - TextInputStyle, - ButtonStyle, -} from "discord-api-types/v10"; -import logger from "../../../core/lib/logger"; -import { CommandMessage } from "../../../core/types/commands"; -import { listVariables } from "../../../core/lib/vars"; -import type Amayo from "../../../core/client"; -import { - BlockState, - DisplayComponentUtils, - EditorActionRow, - DESCRIPTION_PLACEHOLDER, - syncDescriptionComponent, - ensureDescriptionTextComponent, - normalizeDisplayContent, -} from "../../../core/types/displayComponentEditor"; -import type { DisplayComponentContainer } from "../../../core/types/displayComponents"; -import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; - -interface EditorData { - content?: string; - flags?: MessageFlags; - display?: DisplayComponentContainer; - components?: EditorActionRow[]; -} - -// --- Helpers (yald-style minimal generators) --------------------------------- -type ModalField = { - customId: string; - style: number; - placeholder?: string; - value?: string; - required?: boolean; - maxLength?: number; - label?: string; -}; - -function createModal(params: { - title: string; - customId: string; - fields: ModalField[]; -}) { - const components = params.fields.map((f) => ({ - type: ComponentType.Label, - label: f.label ?? "", - component: { - type: ComponentType.TextInput, - customId: f.customId, - style: f.style, - placeholder: f.placeholder, - value: f.value, - required: f.required ?? false, - maxLength: f.maxLength, - }, - })); - return { - title: params.title, - customId: params.customId, - components, - } as const; -} - -function buildSelectOptionsFromComponents(components: any[]) { - return components.map((c: any, idx: number) => ({ - label: - c.type === 10 - ? `Texto: ${c.content?.slice(0, 30) || "..."}` - : c.type === 14 - ? `Separador ${c.divider ? "(Visible)" : "(Invisible)"}` - : c.type === 12 - ? `Imagen: ${c.url?.slice(-30) || "..."}` - : `Componente ${c.type}`, - value: String(idx), - description: - c.type === 10 && (c.thumbnail || c.linkButton) - ? c.thumbnail - ? "Con thumbnail" - : "Con botón link" - : undefined, - })); -} - -// Helper para actualizar el editor combinando Display Container dentro de components (tipado) -async function updateEditor(message: Message, data: EditorData): Promise { - const container = data.display; - const rows = Array.isArray(data.components) ? data.components : []; - const components = container ? [container, ...rows] : rows; - - const payload: any = { ...data }; - delete payload.display; - payload.components = components; - - if (payload.flags === undefined) { - payload.flags = MessageFlags.IsComponentsV2; - } - - // Si usamos Components V2, debemos limpiar explícitamente el content legado en el servidor - if (payload.flags === MessageFlags.IsComponentsV2) { - payload.content = null; - } - - await message.edit(payload); -} - -function stripLegacyDescriptionComponent( - blockState: BlockState, - match?: string | null -): void { - if ( - !Array.isArray(blockState.components) || - blockState.components.length === 0 - ) - return; - - const normalize = (value: string | undefined | null) => - value?.replace(/\s+/g, " ").trim() ?? ""; - const target = normalize(match ?? blockState.description ?? undefined); - if (!target) return; - - const index = blockState.components.findIndex((component: any) => { - if (!component || component.type !== 10) return false; - if (component.thumbnail || component.linkButton) return false; - return normalize(component.content) === target; - }); - - if (index >= 0) { - blockState.components.splice(index, 1); - } -} - -export const command: CommandMessage = { - name: "crear-embed", - type: "message", - aliases: ["embed-crear", "nuevo-embed", "blockcreatev2"], - cooldown: 20, - description: - "Crea un nuevo bloque/embedded con editor interactivo (DisplayComponents).", - category: "Alianzas", - usage: "crear-embed ", - run: async (message, args, client) => { - const allowed = await hasManageGuildOrStaff( - message.member, - message.guild!.id, - client.prisma - ); - if (!allowed) { - await message.reply( - "❌ No tienes permisos de ManageGuild ni rol de staff." - ); - return; - } - - const blockName = args[0]?.trim(); - if (!blockName) { - await message.reply( - "Debes proporcionar un nombre. Uso: `!crear-embed `" - ); - return; - } - - // Check if block name already exists - const existingBlock = await client.prisma.blockV2Config.findFirst({ - where: { - guildId: message.guild!.id, - name: blockName, - }, - }); - - if (existingBlock) { - await message.reply("❌ Ya existe un bloque con ese nombre!"); - return; - } - - // Estado inicial - let blockState: BlockState = { - title: `Editor de Block: ${blockName}`, - color: 0x5865f2, - coverImage: undefined, - components: [ - { type: 14, divider: false, spacing: 1 }, - { type: 10, content: DESCRIPTION_PLACEHOLDER, thumbnail: null }, - ], - }; - - //@ts-ignore - const channelSend: If = - message.channel; - if (!channelSend?.isTextBased()) { - await message.reply( - "❌ This command can only be used in a text-based channel." - ); - return; - } - - const editorMessage = await channelSend.send({ - content: - "⚠️ **IMPORTANTE:** Prepara tus títulos, descripciones y URLs antes de empezar.\n" + - "Este editor usa **modales interactivos** y no podrás ver el chat mientras los usas.\n\n" + - "📝 **Recomendaciones:**\n" + - "• Ten preparados tus títulos y descripciones\n" + - "• Ten las URLs de imágenes listas para copiar\n" + - "• Los colores en formato HEX (#FF5733)\n" + - "• Las variables de usuario/servidor que necesites\n\n" + - "*Iniciando editor en 5 segundos...*", - }); - - // Esperar 5 segundos para que lean el mensaje - await new Promise((resolve) => setTimeout(resolve, 5000)); - - // Actualizar para mostrar el editor - await updateEditor(editorMessage, { - content: undefined, - flags: MessageFlags.IsComponentsV2, - display: await DisplayComponentUtils.renderPreview( - blockState, - message.member!, - message.guild! - ), - components: DisplayComponentUtils.createEditorButtons(false), - }); - - await handleEditorInteractions( - editorMessage, - message, - client, - blockName, - blockState - ); - }, -}; - -async function handleEditorInteractions( - editorMessage: Message, - originalMessage: Message, - client: Amayo, - blockName: string, - blockState: BlockState -): Promise { - const collector = editorMessage.createMessageComponentCollector({ - time: 3600000, // 1 hour - filter: (interaction: MessageComponentInteraction) => - interaction.user.id === originalMessage.author.id, - }); - - collector.on("collect", async (interaction: MessageComponentInteraction) => { - // Verificar que sea una interacción de botón - if (!interaction.isButton()) return; - - try { - await handleButtonInteraction( - interaction, - editorMessage, - originalMessage, - client, - blockName, - blockState - ); - } catch (error) { - //@ts-ignore - logger.error("Error handling editor interaction:", error); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "❌ Ocurrió un error al procesar la interacción.", - flags: MessageFlags.Ephemeral, - }); - } - } - }); - - collector.on("end", async (_collected, reason) => { - if (reason === "time") { - await handleEditorTimeout(editorMessage); - } - }); -} - -async function handleButtonInteraction( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - client: Amayo, - blockName: string, - blockState: BlockState -): Promise { - const { customId } = interaction; - - switch (customId) { - case "edit_title": - await handleEditTitle( - interaction, - editorMessage, - originalMessage, - blockState - ); - break; - - case "edit_description": - await handleEditDescription( - interaction, - editorMessage, - originalMessage, - blockState - ); - break; - - case "edit_color": - await handleEditColor( - interaction, - editorMessage, - originalMessage, - blockState - ); - break; - - case "add_content": - await handleAddContent( - interaction, - editorMessage, - originalMessage, - blockState - ); - break; - - case "add_separator": - await handleAddSeparator( - interaction, - editorMessage, - originalMessage, - blockState - ); - break; - - case "add_image": - await handleAddImage( - interaction, - editorMessage, - originalMessage, - blockState - ); - break; - - case "edit_thumbnail": { - ensureDescriptionTextComponent(blockState, { - placeholder: DESCRIPTION_PLACEHOLDER, - }); - - const descriptionNormalized = normalizeDisplayContent( - blockState.description - ); - const textDisplays = blockState.components - .map((component: any, idx: number) => ({ component, idx })) - .filter(({ component }) => component?.type === 10); - - if (textDisplays.length === 0) { - await interaction - .deferReply({ flags: MessageFlags.Ephemeral }) - .catch(() => {}); - await interaction - .editReply({ - content: - "❌ No hay bloques de texto disponibles para añadir thumbnail.", - }) - .catch(() => {}); - break; - } - - const options = textDisplays.map(({ component, idx }) => ({ - label: - descriptionNormalized && - normalizeDisplayContent(component.content) === descriptionNormalized - ? "Descripción principal" - : `Texto #${idx + 1}: ${component.content?.slice(0, 30) || "..."}`, - value: String(idx), - description: component.thumbnail - ? "Con thumbnail" - : component.linkButton - ? "Con botón link" - : "Sin accesorio", - })); - - try { - await interaction.reply({ - flags: MessageFlags.Ephemeral, - content: - "Selecciona el bloque de texto al que quieres editar el thumbnail:", - components: [ - { - type: 1, - components: [ - { - type: 3, - custom_id: "choose_text_for_thumbnail", - placeholder: "Selecciona un bloque de texto", - options, - }, - ], - }, - ], - }); - } catch (error) { - logger.error({ err: error }, "Error enviando selector de thumbnails"); - break; - } - - let replyMsg: Message | null = null; - try { - replyMsg = await interaction.fetchReply(); - } catch {} - - if (!replyMsg) break; - - const selCollector = replyMsg.createMessageComponentCollector({ - componentType: ComponentType.StringSelect, - max: 1, - time: 60000, - filter: (it: any) => it.user.id === originalMessage.author.id, - }); - - selCollector.on("collect", async (sel: any) => { - selCollector.stop("collected"); - - const idx = parseInt(sel.values[0], 10); - if (Number.isNaN(idx)) { - try { - if (!sel.replied && !sel.deferred) { - await sel.reply({ - content: "❌ Selección inválida.", - flags: MessageFlags.Ephemeral, - }); - } - } catch {} - return; - } - - const textComp = blockState.components[idx]; - if (!textComp || textComp.type !== 10) { - try { - if (!sel.replied && !sel.deferred) { - await sel.reply({ - content: "❌ El bloque seleccionado ya no existe.", - flags: MessageFlags.Ephemeral, - }); - } - } catch {} - return; - } - - const modal = createModal({ - title: "📎 Editar Thumbnail", - customId: `edit_thumbnail_modal_${idx}`, - fields: [ - { - customId: "thumbnail_input", - style: TextInputStyle.Short, - placeholder: - "https://ejemplo.com/thumbnail.png (vacío para eliminar)", - value: textComp.thumbnail || "", - maxLength: 512, - required: false, - label: "URL del Thumbnail", - }, - ], - }); - - try { - await sel.showModal(modal); - } catch (error) { - logger.error( - { err: error }, - "No se pudo mostrar el modal de thumbnail" - ); - return; - } - - const modalInteraction = await awaitModalWithDeferredReply(sel); - if (!modalInteraction) return; - - const rawInput = modalInteraction.components - .getTextInputValue("thumbnail_input") - .trim(); - - if (rawInput.length === 0) { - textComp.thumbnail = null; - await modalInteraction - .editReply({ content: "✅ Thumbnail eliminado." }) - .catch(() => {}); - } else if (!DisplayComponentUtils.isValidUrl(rawInput)) { - await modalInteraction - .editReply({ content: "❌ URL de thumbnail inválida." }) - .catch(() => {}); - return; - } else if (textComp.linkButton) { - await modalInteraction - .editReply({ - content: - "❌ Este bloque tiene un botón link. Elimínalo antes de añadir un thumbnail.", - }) - .catch(() => {}); - return; - } else { - textComp.thumbnail = rawInput; - await modalInteraction - .editReply({ content: "✅ Thumbnail actualizado." }) - .catch(() => {}); - } - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview( - blockState, - originalMessage.member!, - originalMessage.guild! - ), - components: DisplayComponentUtils.createEditorButtons(false), - }); - }); - - selCollector.on("end", async () => { - try { - await replyMsg!.edit({ components: [] }); - } catch {} - }); - - break; - } - - case "cover_image": - await handleCoverImage( - interaction, - editorMessage, - originalMessage, - blockState - ); - break; - - case "move_block": { - const options = buildSelectOptionsFromComponents(blockState.components); - - await interaction.reply({ - flags: MessageFlags.Ephemeral, - content: "Selecciona el bloque que quieres mover:", - components: [ - { - type: 1, - components: [ - { - type: 3, - custom_id: "move_block_select", - placeholder: "Elige un bloque", - options, - }, - ], - }, - ], - }); - const replyMsg = await interaction.fetchReply(); - // @ts-ignore - const selCollector = replyMsg.createMessageComponentCollector({ - componentType: ComponentType.StringSelect, - max: 1, - time: 60000, - filter: (it: any) => it.user.id === originalMessage.author.id, - }); - selCollector.on("collect", async (sel: any) => { - const idx = parseInt(sel.values[0]); - await sel.update({ - content: "¿Quieres mover este bloque?", - components: [ - { - type: 1, - components: [ - { - type: 2, - style: ButtonStyle.Secondary, - label: "⬆️ Subir", - custom_id: `move_up_${idx}`, - disabled: idx === 0, - }, - { - type: 2, - style: ButtonStyle.Secondary, - label: "⬇️ Bajar", - custom_id: `move_down_${idx}`, - disabled: idx === blockState.components.length - 1, - }, - ], - }, - ], - }); - // @ts-ignore - const btnCollector = replyMsg.createMessageComponentCollector({ - componentType: ComponentType.Button, - max: 1, - time: 60000, - filter: (b: any) => b.user.id === originalMessage.author.id, - }); - btnCollector.on("collect", async (b: any) => { - if (b.customId.startsWith("move_up_")) { - const i2 = parseInt(b.customId.replace("move_up_", "")); - if (i2 > 0) { - const item = blockState.components[i2]; - blockState.components.splice(i2, 1); - blockState.components.splice(i2 - 1, 0, item); - } - await b.update({ - content: "✅ Bloque movido arriba.", - components: [], - }); - } else if (b.customId.startsWith("move_down_")) { - const i2 = parseInt(b.customId.replace("move_down_", "")); - if (i2 < blockState.components.length - 1) { - const item = blockState.components[i2]; - blockState.components.splice(i2, 1); - blockState.components.splice(i2 + 1, 0, item); - } - await b.update({ - content: "✅ Bloque movido abajo.", - components: [], - }); - } - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview( - blockState, - originalMessage.member!, - originalMessage.guild! - ), - components: DisplayComponentUtils.createEditorButtons(false), - }); - btnCollector.stop(); - selCollector.stop(); - }); - }); - break; - } - - case "delete_block": { - const options: any[] = []; - if (blockState.coverImage) - options.push({ - label: "🖼️ Imagen de Portada", - value: "cover_image", - description: "Imagen principal del bloque", - }); - options.push(...buildSelectOptionsFromComponents(blockState.components)); - - if (options.length === 0) { - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - // @ts-ignore - await interaction.editReply({ - content: "❌ No hay elementos para eliminar.", - }); - break; - } - - await interaction.reply({ - flags: MessageFlags.Ephemeral, - content: "Selecciona el elemento que quieres eliminar:", - components: [ - { - type: 1, - components: [ - { - type: 3, - custom_id: "delete_block_select", - placeholder: "Elige un elemento", - options, - }, - ], - }, - ], - }); - const replyMsg = await interaction.fetchReply(); - // @ts-ignore - const selCollector = replyMsg.createMessageComponentCollector({ - componentType: ComponentType.StringSelect, - max: 1, - time: 60000, - filter: (it: any) => it.user.id === originalMessage.author.id, - }); - selCollector.on("collect", async (sel: any) => { - const selectedValue = sel.values[0]; - if (selectedValue === "cover_image") { - // @ts-ignore - blockState.coverImage = null; - await sel.update({ - content: "✅ Imagen de portada eliminada.", - components: [], - }); - } else { - const idx = parseInt(selectedValue); - blockState.components.splice(idx, 1); - await sel.update({ - content: "✅ Elemento eliminado.", - components: [], - }); - } - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview( - blockState, - originalMessage.member!, - originalMessage.guild! - ), - components: DisplayComponentUtils.createEditorButtons(false), - }); - selCollector.stop(); - }); - break; - } - - case "show_variables": - await handleShowVariables(interaction); - break; - - case "show_raw": - await handleShowRaw(interaction, blockState); - break; - - case "save_block": - await handleSaveBlock( - interaction, - editorMessage, - client, - blockName, - blockState, - originalMessage.guildId! - ); - break; - - case "cancel_block": - await handleCancelBlock(interaction, editorMessage); - break; - - default: - await interaction.reply({ - content: `⚠️ Funcionalidad \`${customId}\` en desarrollo.`, - flags: MessageFlags.Ephemeral, - }); - break; - } -} - -async function awaitModalWithDeferredReply( - interaction: ButtonInteraction | MessageComponentInteraction, - options: Parameters[0] = { - time: 300000, - } -): Promise { - try { - const modalInteraction = await interaction.awaitModalSubmit(options); - if (!modalInteraction.deferred && !modalInteraction.replied) { - await modalInteraction.deferReply({ flags: MessageFlags.Ephemeral }); - } - return modalInteraction; - } catch (error) { - if ( - !(error instanceof Error) || - !error.message.includes("Collector received no interactions") - ) { - logger.error({ err: error }, "Error esperando envío de modal en editor"); - } - return null; - } -} - -async function handleEditTitle( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = createModal({ - title: "Editar Título del Bloque", - customId: "edit_title_modal", - fields: [ - { - customId: "title_input", - style: TextInputStyle.Short, - required: true, - placeholder: "Escribe el título del bloque...", - value: blockState.title || "", - maxLength: 256, - label: "Título", - }, - ], - }); - - await interaction.showModal(modal as any); - - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; - - const newTitle = modalInteraction.components - .getTextInputValue("title_input") - .trim(); - - if (newTitle) { - blockState.title = newTitle; - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview( - blockState, - originalMessage.member!, - originalMessage.guild! - ), - components: DisplayComponentUtils.createEditorButtons(false), - }); - } - - await modalInteraction.editReply({ - content: "✅ Título actualizado correctamente.", - }); - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction - .editReply({ - content: "❌ No se pudo actualizar el título. Inténtalo de nuevo.", - }) - .catch(() => {}); - } - if ( - error instanceof Error && - error.message.includes("Collector received no interactions") - ) { - return; - } - logger.error({ err: error }, "Error procesando modal de título"); - } -} - -async function handleEditDescription( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = { - title: "Editar Descripción del Bloque", - customId: "edit_description_modal", - components: [ - { - type: ComponentType.Label, - label: "Descripción", - component: { - type: ComponentType.TextInput, - customId: "description_input", - style: TextInputStyle.Paragraph, - required: false, - placeholder: "Escribe la descripción del bloque...", - value: blockState.description || "", - maxLength: 4000, - }, - }, - ], - } as const; - - await interaction.showModal(modal); - - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; - - const rawDescription = - modalInteraction.components.getTextInputValue("description_input"); - const previousDescription = - typeof blockState.description === "string" - ? blockState.description - : null; - syncDescriptionComponent(blockState, rawDescription, { - previousDescription, - placeholder: DESCRIPTION_PLACEHOLDER, - }); - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview( - blockState, - originalMessage.member!, - originalMessage.guild! - ), - components: DisplayComponentUtils.createEditorButtons(false), - }); - - await modalInteraction.editReply({ - content: "✅ Descripción actualizada correctamente.", - }); - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction - .editReply({ - content: - "❌ No se pudo actualizar la descripción. Inténtalo de nuevo.", - }) - .catch(() => {}); - } - if ( - error instanceof Error && - error.message.includes("Collector received no interactions") - ) { - return; - } - logger.error({ err: error }, "Error procesando modal de descripción"); - } -} - -async function handleEditColor( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = { - title: "Editar Color del Bloque", - customId: "edit_color_modal", - components: [ - { - type: ComponentType.Label, - label: "Color (formato HEX)", - component: { - type: ComponentType.TextInput, - customId: "color_input", - style: TextInputStyle.Short, - required: false, - placeholder: "#FF5733 o FF5733", - value: blockState.color - ? `#${blockState.color.toString(16).padStart(6, "0")}` - : "", - maxLength: 7, - }, - }, - ], - } as const; - - await interaction.showModal(modal); - - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; - - const colorValue = modalInteraction.components - .getTextInputValue("color_input") - .trim(); - - if (colorValue) { - const cleanColor = colorValue.replace("#", ""); - const colorNumber = parseInt(cleanColor, 16); - - if (!isNaN(colorNumber) && cleanColor.length === 6) { - blockState.color = colorNumber; - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview( - blockState, - originalMessage.member!, - originalMessage.guild! - ), - components: DisplayComponentUtils.createEditorButtons(false), - }); - - await modalInteraction.editReply({ - content: "✅ Color actualizado correctamente.", - }); - } else { - await modalInteraction.editReply({ - content: "❌ Color inválido. Usa formato HEX como #FF5733", - }); - } - } else { - blockState.color = undefined; - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview( - blockState, - originalMessage.member!, - originalMessage.guild! - ), - components: DisplayComponentUtils.createEditorButtons(false), - }); - - await modalInteraction.editReply({ - content: "✅ Color removido.", - }); - } - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction - .editReply({ - content: "❌ No se pudo actualizar el color. Inténtalo de nuevo.", - }) - .catch(() => {}); - } - if ( - error instanceof Error && - error.message.includes("Collector received no interactions") - ) { - return; - } - logger.error({ err: error }, "Error procesando modal de color"); - } -} - -async function handleAddContent( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = { - title: "Añadir Contenido de Texto", - customId: "add_content_modal", - components: [ - { - type: ComponentType.Label, - label: "Contenido", - component: { - type: ComponentType.TextInput, - customId: "content_input", - style: TextInputStyle.Paragraph, - required: true, - placeholder: "Escribe el contenido de texto...", - maxLength: 4000, - }, - }, - ], - } as const; - - await interaction.showModal(modal); - - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; - - const content = modalInteraction.components - .getTextInputValue("content_input") - .trim(); - - if (content) { - blockState.components.push({ - type: 10, - content, - thumbnail: null, - }); - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview( - blockState, - originalMessage.member!, - originalMessage.guild! - ), - components: DisplayComponentUtils.createEditorButtons(false), - }); - - await modalInteraction.editReply({ - content: "✅ Contenido añadido correctamente.", - }); - } - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction - .editReply({ - content: "❌ No se pudo añadir el contenido. Inténtalo de nuevo.", - }) - .catch(() => {}); - } - if ( - error instanceof Error && - error.message.includes("Collector received no interactions") - ) { - return; - } - logger.error({ err: error }, "Error procesando modal de contenido"); - } -} - -async function handleAddSeparator( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const wasAcknowledged = interaction.deferred || interaction.replied; - if (!wasAcknowledged) { - try { - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - } catch (error) { - logger.warn( - { err: error }, - "No se pudo diferir respuesta al añadir separador" - ); - } - } - - blockState.components.push({ - type: 14, - divider: true, - spacing: 1, - }); - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview( - blockState, - originalMessage.member!, - originalMessage.guild! - ), - components: DisplayComponentUtils.createEditorButtons(false), - }); - - const payload = { - content: "✅ Separador añadido correctamente.", - flags: MessageFlags.Ephemeral, - } as const; - - if (interaction.deferred) { - await interaction.editReply({ content: payload.content }).catch(() => {}); - } else if (interaction.replied) { - await interaction.followUp(payload).catch(() => {}); - } else { - await interaction.reply(payload).catch(() => {}); - } -} - -async function handleAddImage( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = createModal({ - title: "Añadir Imagen", - customId: "add_image_modal", - fields: [ - { - customId: "image_input", - style: TextInputStyle.Short, - required: true, - placeholder: "https://ejemplo.com/imagen.png", - maxLength: 512, - label: "URL de la Imagen", - }, - ], - }); - - await interaction.showModal(modal as any); - - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; - - const imageUrl = modalInteraction.components - .getTextInputValue("image_input") - .trim(); - - if (imageUrl && DisplayComponentUtils.isValidUrl(imageUrl)) { - blockState.components.push({ - type: 12, - url: imageUrl, - }); - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview( - blockState, - originalMessage.member!, - originalMessage.guild! - ), - components: DisplayComponentUtils.createEditorButtons(false), - }); - - await modalInteraction.editReply({ - content: "✅ Imagen añadida correctamente.", - }); - } else { - await modalInteraction.editReply({ - content: "❌ URL de imagen inválida.", - }); - } - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction - .editReply({ - content: "❌ No se pudo añadir la imagen. Inténtalo de nuevo.", - }) - .catch(() => {}); - } - if ( - error instanceof Error && - error.message.includes("Collector received no interactions") - ) { - return; - } - logger.error({ err: error }, "Error procesando modal de imagen"); - } -} - -async function handleCoverImage( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = { - title: "Imagen de Portada", - customId: "cover_image_modal", - components: [ - { - type: ComponentType.Label, - label: "URL de la Imagen de Portada", - component: { - type: ComponentType.TextInput, - customId: "cover_input", - style: TextInputStyle.Short, - required: false, - placeholder: "https://ejemplo.com/portada.png", - value: blockState.coverImage || "", - maxLength: 512, - }, - }, - ], - } as const; - - await interaction.showModal(modal); - - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; - - const coverUrl = modalInteraction.components - .getTextInputValue("cover_input") - .trim(); - - if (coverUrl && DisplayComponentUtils.isValidUrl(coverUrl)) { - blockState.coverImage = coverUrl; - } else { - blockState.coverImage = undefined; - } - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview( - blockState, - originalMessage.member!, - originalMessage.guild! - ), - components: DisplayComponentUtils.createEditorButtons(false), - }); - - await modalInteraction.editReply({ - content: coverUrl - ? "✅ Imagen de portada actualizada." - : "✅ Imagen de portada removida.", - }); - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction - .editReply({ - content: - "❌ No se pudo actualizar la imagen de portada. Inténtalo de nuevo.", - }) - .catch(() => {}); - } - if ( - error instanceof Error && - error.message.includes("Collector received no interactions") - ) { - return; - } - logger.error({ err: error }, "Error procesando modal de portada"); - } -} - -async function handleShowVariables( - interaction: ButtonInteraction -): Promise { - const variables = listVariables(); - await interaction.reply({ - content: `📋 **Variables disponibles:**\n\`\`\`\n${variables}\`\`\``, - flags: MessageFlags.Ephemeral, - }); -} - -async function handleShowRaw( - interaction: ButtonInteraction, - blockState: BlockState -): Promise { - const rawData = JSON.stringify(blockState, null, 2); - await interaction.reply({ - content: `📊 **Datos del bloque:**\n\`\`\`json\n${rawData.slice( - 0, - 1800 - )}\`\`\``, - flags: MessageFlags.Ephemeral, - }); -} - -async function handleSaveBlock( - interaction: ButtonInteraction, - editorMessage: Message, - client: Amayo, - blockName: string, - blockState: BlockState, - guildId: string -): Promise { - try { - stripLegacyDescriptionComponent(blockState); - await client.prisma.blockV2Config.create({ - data: { - guildId, - name: blockName, - config: blockState as any, - }, - }); - - await interaction.reply({ - content: `✅ **Bloque guardado exitosamente!**\n\n📄 **Nombre:** \`${blockName}\`\n🎨 **Componentes:** ${blockState.components.length}\n\n🎯 **Uso:** \`!send-embed ${blockName}\``, - flags: MessageFlags.Ephemeral, - }); - - // Cerrar el editor eliminando el mensaje del editor - try { - await editorMessage.delete(); - } catch {} - } catch (error) { - //@ts-ignore - logger.error("Error saving block:", error); - await interaction.reply({ - content: "❌ Error al guardar el bloque. Inténtalo de nuevo.", - flags: MessageFlags.Ephemeral, - }); - } -} - -async function handleCancelBlock( - interaction: ButtonInteraction, - editorMessage: Message -): Promise { - try { - await interaction.deferUpdate(); - } catch {} - await updateEditor(editorMessage, { - display: { - type: 17, - components: [ - { type: 10, content: "❌ **Editor cancelado**" }, - { type: 10, content: "La creación del bloque ha sido cancelada." }, - ], - } as any, - components: [], - }); -} - -async function handleEditorTimeout(editorMessage: Message): Promise { - try { - await updateEditor(editorMessage, { - display: { - type: 17, - components: [ - { type: 10, content: "⏰ **Editor expirado**" }, - { - type: 10, - content: - "El editor ha expirado por inactividad. Usa el comando nuevamente para crear un bloque.", - }, - ], - } as any, - components: [], - }); - } catch { - // message likely deleted - } -} diff --git a/src/test_help_logic.ts b/src/test_help_logic.ts deleted file mode 100644 index 2e5a4b9..0000000 --- a/src/test_help_logic.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { commands as registry } from "../commands/messages/help"; // This won't work directly because help.ts exports 'command' not 'registry' -// We need to test the logic, not the file directly if it has side effects. -// Instead, let's just verify the imports and logic structure. - -import { registry as varRegistry } from "../core/lib/variables/registry"; -import { commands } from "../core/loaders/loader"; - -async function testHelpLogic() { - console.log("--- Testing Help Command Logic ---"); - - // 1. Test Variable Registry - const allVars = varRegistry.list(); - console.log(`Found ${allVars.length} variables`); - - const varCategories = new Map(); - for (const v of allVars) { - const [cat] = v.split("."); - const categoryName = cat.charAt(0).toUpperCase() + cat.slice(1); - if (!varCategories.has(categoryName)) varCategories.set(categoryName, []); - varCategories.get(categoryName)!.push(v); - } - console.log(`Variable Categories: ${Array.from(varCategories.keys()).join(", ")}`); - - // 2. Test Command Registry (Mocking if empty) - console.log(`Found ${commands.size} commands in loader`); - - console.log("--- Help Logic Test Passed ---"); -} - -testHelpLogic(); diff --git a/test/.validate_dashboard_items_syntax.js b/test/.validate_dashboard_items_syntax.js deleted file mode 100644 index c4f2b33..0000000 --- a/test/.validate_dashboard_items_syntax.js +++ /dev/null @@ -1,84 +0,0 @@ -const fs = require('fs'); -const path = 'src/server/views/partials/dashboard/dashboard_items.ejs'; -try{ - const s = fs.readFileSync(path, 'utf8'); - const m = s.match(/]*>([\s\S]*?)<\/script>/i); - if (!m) { console.error('NO_SCRIPT'); process.exit(2); } - let script = m[1]; - // replace EJS output tags with empty string literal and remove other EJS tags - script = script.replace(/<%=([\s\S]*?)%>/g, "''"); - script = script.replace(/<%[\s\S]*?%>/g, ''); - try { - // quick balance scanner to find likely unclosed tokens - (function scan() { - const s2 = script; - const stack = []; - let inSingle = false, inDouble = false, inTpl = false; - let inLineComment = false, inBlockComment = false; - for (let i = 0; i < s2.length; i++) { - const ch = s2[i]; - const prev = s2[i-1]; - // comments handling - if (!inSingle && !inDouble && !inTpl) { - if (!inBlockComment && ch === '/' && s2[i+1] === '/') { inLineComment = true; continue; } - if (!inLineComment && ch === '/' && s2[i+1] === '*') { inBlockComment = true; i++; continue; } - } - if (inLineComment) { if (ch === '\n') inLineComment = false; continue; } - if (inBlockComment) { if (ch === '*' && s2[i+1] === '/') { inBlockComment = false; i++; } continue; } - // string toggles - if (!inDouble && !inTpl && ch === '\'' && prev !== '\\') { inSingle = !inSingle; continue; } - if (!inSingle && !inTpl && ch === '"' && prev !== '\\') { inDouble = !inDouble; continue; } - if (!inSingle && !inDouble && ch === '`' && prev !== '\\') { inTpl = !inTpl; continue; } - if (inSingle || inDouble || inTpl) continue; - // brackets - if (ch === '(' || ch === '{' || ch === '[') stack.push({ ch, pos: i }); - if (ch === ')' || ch === '}' || ch === ']') { - const last = stack.pop(); - if (!last) { console.error('UNMATCHED_CLOSE', ch, 'at', i); break; } - const map = { '(':')','{':'}','[':']' }; - if (map[last.ch] !== ch) { console.error('MISMATCH', last.ch, 'opened at', last.pos, 'but closed by', ch, 'at', i); break; } - } - } - if (inSingle || inDouble || inTpl) console.error('UNTERMINATED_STRING_OR_TEMPLATE'); - if (inBlockComment) console.error('UNTERMINATED_BLOCK_COMMENT'); - if (stack.length) { - const last = stack[stack.length-1]; - // compute line/col - const upTo = s2.slice(0, last.pos); - const line = upTo.split('\n').length; - const col = last.pos - upTo.lastIndexOf('\n'); - console.error('UNMATCHED_OPEN', last.ch, 'at index', last.pos, 'line', line, 'col', col); - const context = s2.slice(Math.max(0, last.pos-40), Math.min(s2.length, last.pos+40)).replace(/\n/g, '\\n'); - console.error('CONTEXT:', context); - } - })(); - - // try acorn parse first for better diagnostics (if available) - try { - const acorn = require('acorn'); - acorn.parse(script, { ecmaVersion: 2020 }); - } catch (eac) { - console.error('ACORN_PARSE_ERROR:' + (eac && eac.message ? eac.message : String(eac))); - if (eac && eac.loc) console.error('loc', eac.loc); - // fallthrough to vm.Script for older Node versions - } - const vm = require('vm'); - new vm.Script(script, { filename: 'dashboard_items_script.js' }); - console.log('OK'); - process.exit(0); - } catch (e) { - console.error('SYNTAX_ERROR:' + (e && e.message ? e.message : String(e))); - if (e && e.stack) console.error(e.stack); - if (e && typeof e.lineNumber !== 'undefined') console.error('line:' + e.lineNumber + ' col:' + (e.columnNumber||'?')); - // print first 200 lines for inspection - const lines = script.split('\n'); - for (let i = 0; i < Math.min(lines.length, 400); i++) { - const n = (i+1).toString().padStart(4, ' '); - console.error(n + ': ' + lines[i]); - } - process.exit(3); - } -} catch (e) { - console.error('READ_ERROR:' + e.message); - process.exit(4); -} diff --git a/test/example.ts.txt b/test/example.ts.txt deleted file mode 100644 index 08c5439..0000000 --- a/test/example.ts.txt +++ /dev/null @@ -1,145 +0,0 @@ -import { Client, GatewayIntentBits, Events } from 'discord.js'; -import { ButtonStyle, ComponentType, TextInputStyle, ChannelType } from 'discord-api-types/v10'; - -const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] }); - -client.once(Events.ClientReady, () => { - console.log(`Bot listo como ${client.user?.tag}`); -}); - -client.on(Events.MessageCreate, async (message) => { - if (message.author.bot) return; - if (message.content === '!modal') { - // Enviar un botón (sin Builders) para abrir el modal - await message.channel.send({ - content: 'Haz clic para abrir el modal:', - components: [ - { - type: ComponentType.ActionRow, - components: [ - { - type: ComponentType.Button, - custom_id: 'openModal', - label: 'Abrir modal', - style: ButtonStyle.Primary, - }, - ], - }, - ], - }); - } -}); - -client.on(Events.InteractionCreate, async (interaction) => { - // Abrir el modal cuando se presiona el botón - if (interaction.isButton() && interaction.customId === 'openModal') { - const modal = { - title: 'Modal avanzado', - customId: 'miModal', - components: [ - { type: ComponentType.TextDisplay, content: 'Lee esto antes de continuar.' }, - { - type: ComponentType.Label, - label: 'Escribe algo', - component: { - type: ComponentType.TextInput, - customId: 'input1', - style: TextInputStyle.Short, - required: true, - placeholder: 'Tu respuesta...', - }, - }, - { - type: ComponentType.Label, - label: 'Selecciona usuarios (máx 3)', - component: { - type: ComponentType.UserSelect, - customId: 'users', - required: false, - minValues: 0, - maxValues: 3, - placeholder: 'Usuarios...', - }, - }, - { - type: ComponentType.Label, - label: 'Selecciona roles (máx 2)', - component: { - type: ComponentType.RoleSelect, - customId: 'roles', - required: false, - minValues: 0, - maxValues: 2, - placeholder: 'Roles...', - }, - }, - { - type: ComponentType.Label, - label: 'Selecciona canales de texto', - component: { - type: ComponentType.ChannelSelect, - customId: 'channels', - required: false, - minValues: 0, - maxValues: 2, - placeholder: 'Canales...', - channelTypes: [ChannelType.GuildText, ChannelType.PublicThread, ChannelType.PrivateThread], - }, - }, - ], - } as const; - - await interaction.showModal(modal); - return; - } - - // Manejar el envío del modal - if (interaction.isModalSubmit() && interaction.customId === 'miModal') { - const textValue = interaction.components.getTextInputValue('input1'); - - const users = interaction.components.getSelectedUsers('users'); - const roles = interaction.components.getSelectedRoles('roles'); - const channels = interaction.components.getSelectedChannels('channels', false, [ - ChannelType.GuildText, - ChannelType.PublicThread, - ChannelType.PrivateThread, - ]); - - const usersStr = users - ? Array.from(users.values()) - .filter(Boolean) - .map((u: any) => u?.tag ?? u?.username ?? u?.id) - .join(', ') - : 'ninguno'; - const rolesStr = roles - ? Array.from(roles.values()) - .filter(Boolean) - .map((r: any) => r?.name ?? r?.id) - .join(', ') - : 'ninguno'; - const channelsStr = channels - ? Array.from(channels.values()) - .filter(Boolean) - .map((c: any) => c?.name ?? c?.id) - .join(', ') - : 'ninguno'; - - await interaction.reply({ - content: [ - `Texto: ${textValue}`, - `Usuarios: ${usersStr}`, - `Roles: ${rolesStr}`, - `Canales: ${channelsStr}`, - ].join('\n'), - flags: 64, // Ephemeral - }); - } -}); - -process.loadEnvFile() -const token = process.env.DISCORD_TOKEN; -if (!token) { - console.error('Falta la variable de entorno DISCORD_TOKEN'); - process.exit(1); -} -client.login(token); diff --git a/test/examples/featureFlagsCommands.ts b/test/examples/featureFlagsCommands.ts deleted file mode 100644 index c748111..0000000 --- a/test/examples/featureFlagsCommands.ts +++ /dev/null @@ -1,359 +0,0 @@ -/** - * Ejemplos de uso de Feature Flags en comandos - * Funciona para comandos slash Y comandos de mensaje - */ - -import { ChatInputCommandInteraction, Message } from "discord.js"; -import { CommandSlash, CommandMessage } from "../../src/core/types/commands"; -import { - withFeatureFlag, - checkFeatureFlag, - guardFeatureFlag, - abTestCommand, -} from "../../src/core/lib/featureFlagCommandWrapper"; -import type Amayo from "../../src/core/client"; - -// ============================================================================ -// PATRÓN 1: Usando withFeatureFlag (wrapper) - RECOMENDADO -// ============================================================================ - -/** - * Comando Slash con Feature Flag - * El wrapper bloquea automáticamente si el flag está disabled - */ -export const shopSlashCommand: CommandSlash = { - name: "shop", - description: "Abre la tienda", - type: "slash", - cooldown: 10, - // Envuelve el handler con el wrapper - run: withFeatureFlag( - "new_shop_system", - async (interaction: ChatInputCommandInteraction, client: Amayo) => { - // Este código solo se ejecuta si el flag está enabled - await interaction.reply("🛒 Bienvenido a la tienda!"); - }, - { - fallbackMessage: "🔧 La tienda está en mantenimiento.", - } - ), -}; - -/** - * Comando de Mensaje con Feature Flag - * El mismo wrapper funciona para comandos de mensaje - */ -export const shopMessageCommand: CommandMessage = { - name: "shop", - type: "message", - cooldown: 10, - description: "Abre la tienda", - // El mismo wrapper funciona aquí - run: withFeatureFlag( - "new_shop_system", - async (message: Message, args: string[], client: Amayo) => { - // Este código solo se ejecuta si el flag está enabled - await message.reply("🛒 Bienvenido a la tienda!"); - }, - { - fallbackMessage: "🔧 La tienda está en mantenimiento.", - } - ), -}; - -// ============================================================================ -// PATRÓN 2: Usando guardFeatureFlag (check con respuesta automática) -// ============================================================================ - -/** - * Comando Slash con guard - */ -export const mineSlashCommand: CommandSlash = { - name: "mine", - description: "Minea recursos", - type: "slash", - cooldown: 10, - run: async (interaction, client) => { - // Guard que responde automáticamente si está disabled - if (!(await guardFeatureFlag("new_mining_system", interaction))) { - return; // Ya respondió automáticamente - } - - // Código del comando - await interaction.reply("⛏️ Minando..."); - }, -}; - -/** - * Comando de Mensaje con guard - */ -export const mineMessageCommand: CommandMessage = { - name: "mine", - type: "message", - cooldown: 10, - run: async (message, args, client) => { - // El mismo guard funciona para mensajes - if (!(await guardFeatureFlag("new_mining_system", message))) { - return; - } - - await message.reply("⛏️ Minando..."); - }, -}; - -// ============================================================================ -// PATRÓN 3: Usando checkFeatureFlag (check manual) -// ============================================================================ - -/** - * Comando Slash con check manual - * Útil cuando necesitas lógica custom - */ -export const inventorySlashCommand: CommandSlash = { - name: "inventory", - description: "Muestra tu inventario", - type: "slash", - cooldown: 5, - run: async (interaction, client) => { - const useNewUI = await checkFeatureFlag("inventory_ui_v2", interaction); - - if (useNewUI) { - // Nueva UI - await interaction.reply({ - content: "📦 **Inventario v2**\n- Item 1\n- Item 2", - }); - } else { - // UI antigua - await interaction.reply({ - content: "📦 Inventario: Item 1, Item 2", - }); - } - }, -}; - -/** - * Comando de Mensaje con check manual - */ -export const inventoryMessageCommand: CommandMessage = { - name: "inventory", - type: "message", - cooldown: 5, - aliases: ["inv", "items"], - run: async (message, args, client) => { - const useNewUI = await checkFeatureFlag("inventory_ui_v2", message); - - if (useNewUI) { - await message.reply("📦 **Inventario v2**\n- Item 1\n- Item 2"); - } else { - await message.reply("📦 Inventario: Item 1, Item 2"); - } - }, -}; - -// ============================================================================ -// PATRÓN 4: A/B Testing -// ============================================================================ - -/** - * Comando Slash con A/B testing - */ -export const combatSlashCommand: CommandSlash = { - name: "attack", - description: "Ataca a un enemigo", - type: "slash", - cooldown: 10, - run: async (interaction, client) => { - await abTestCommand("improved_combat_algorithm", interaction, { - variant: async () => { - // 50% de usuarios ven el nuevo algoritmo - const damage = Math.floor(Math.random() * 100) + 50; - await interaction.reply(`⚔️ Daño (nuevo): ${damage}`); - }, - control: async () => { - // 50% ven el algoritmo antiguo - const damage = Math.floor(Math.random() * 50) + 25; - await interaction.reply(`⚔️ Daño (antiguo): ${damage}`); - }, - }); - }, -}; - -/** - * Comando de Mensaje con A/B testing - */ -export const combatMessageCommand: CommandMessage = { - name: "attack", - type: "message", - cooldown: 10, - run: async (message, args, client) => { - await abTestCommand("improved_combat_algorithm", message, { - variant: async () => { - const damage = Math.floor(Math.random() * 100) + 50; - await message.reply(`⚔️ Daño (nuevo): ${damage}`); - }, - control: async () => { - const damage = Math.floor(Math.random() * 50) + 25; - await message.reply(`⚔️ Daño (antiguo): ${damage}`); - }, - }); - }, -}; - -// ============================================================================ -// PATRÓN 5: Múltiples flags (migrando sistema antiguo a nuevo) -// ============================================================================ - -/** - * Comando que migra gradualmente de un sistema a otro - */ -export const economySlashCommand: CommandSlash = { - name: "balance", - description: "Muestra tu balance", - type: "slash", - cooldown: 5, - run: async (interaction, client) => { - const useNewEconomy = await checkFeatureFlag( - "economy_system_v2", - interaction - ); - const usePremiumFeatures = await checkFeatureFlag( - "premium_features", - interaction - ); - - if (useNewEconomy) { - // Sistema nuevo de economía - const balance = 5000; - const streak = usePremiumFeatures ? "🔥 Racha: 7 días" : ""; - - await interaction.reply( - `💰 Balance: ${balance} monedas\n${streak}`.trim() - ); - } else { - // Sistema antiguo - const balance = 5000; - await interaction.reply(`💰 Tienes ${balance} monedas`); - } - }, -}; - -/** - * Lo mismo pero para comando de mensaje - */ -export const economyMessageCommand: CommandMessage = { - name: "balance", - type: "message", - cooldown: 5, - aliases: ["bal", "money"], - run: async (message, args, client) => { - const useNewEconomy = await checkFeatureFlag("economy_system_v2", message); - const usePremiumFeatures = await checkFeatureFlag( - "premium_features", - message - ); - - if (useNewEconomy) { - const balance = 5000; - const streak = usePremiumFeatures ? "🔥 Racha: 7 días" : ""; - await message.reply(`💰 Balance: ${balance} monedas\n${streak}`.trim()); - } else { - const balance = 5000; - await message.reply(`💰 Tienes ${balance} monedas`); - } - }, -}; - -// ============================================================================ -// PATRÓN 6: Comando universal (un solo run para ambos) -// ============================================================================ - -/** - * Helper para detectar tipo de comando - */ -function isSlashCommand( - source: ChatInputCommandInteraction | Message -): source is ChatInputCommandInteraction { - return "options" in source && "user" in source; -} - -/** - * Función de negocio universal - */ -async function showProfile( - source: ChatInputCommandInteraction | Message, - userId: string -) { - const useNewProfile = await checkFeatureFlag("profile_v2", source); - - const profileText = useNewProfile - ? `👤 **Perfil v2**\nUsuario: <@${userId}>\nNivel: 10` - : `👤 Perfil: <@${userId}> - Nivel 10`; - - if (isSlashCommand(source)) { - await source.reply(profileText); - } else { - await source.reply(profileText); - } -} - -/** - * Comando Slash que usa la función universal - */ -export const profileSlashCommand: CommandSlash = { - name: "profile", - description: "Muestra tu perfil", - type: "slash", - cooldown: 5, - run: async (interaction, client) => { - await showProfile(interaction, interaction.user.id); - }, -}; - -/** - * Comando de Mensaje que usa la misma función universal - */ -export const profileMessageCommand: CommandMessage = { - name: "profile", - type: "message", - cooldown: 5, - aliases: ["perfil", "me"], - run: async (message, args, client) => { - await showProfile(message, message.author.id); - }, -}; - -// ============================================================================ -// RESUMEN DE PATRONES -// ============================================================================ - -/* - * PATRÓN 1: withFeatureFlag() - * - Más limpio y declarativo - * - Bloquea automáticamente si disabled - * - Recomendado para comandos simples - * - * PATRÓN 2: guardFeatureFlag() - * - Check con respuesta automática - * - Control total del flujo - * - Bueno para lógica compleja - * - * PATRÓN 3: checkFeatureFlag() - * - Check manual sin respuesta - * - Para if/else personalizados - * - Migración gradual de sistemas - * - * PATRÓN 4: abTestCommand() - * - A/B testing directo - * - Ejecuta función u otra según flag - * - Ideal para comparar versiones - * - * PATRÓN 5: Múltiples flags - * - Combina varios checks - * - Features progresivas - * - Sistemas modulares - * - * PATRÓN 6: Función universal - * - Un solo código para ambos tipos - * - Reutilización máxima - * - Mantenimiento simplificado - */ diff --git a/test/invalid_mobs_backup.json b/test/invalid_mobs_backup.json deleted file mode 100644 index 49d6dda..0000000 --- a/test/invalid_mobs_backup.json +++ /dev/null @@ -1,131 +0,0 @@ -[ - { - "id": "cmgqyuj2h000zmofwe8oflv0r", - "error": { - "name": "ZodError", - "message": "[\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"key\"\n ],\n \"message\": \"Invalid input: expected string, received undefined\"\n },\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"name\"\n ],\n \"message\": \"Invalid input: expected string, received undefined\"\n },\n {\n \"expected\": \"number\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"tier\"\n ],\n \"message\": \"Invalid input: expected number, received undefined\"\n },\n {\n \"expected\": \"object\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"base\"\n ],\n \"message\": \"Invalid input: expected object, received undefined\"\n }\n]" - }, - "row": { - "id": "cmgqyuj2h000zmofwe8oflv0r", - "key": "bat", - "name": "Murciélago", - "category": null, - "guildId": "1316592320954630144", - "stats": { - "attack": 4 - }, - "drops": null, - "metadata": null, - "createdAt": "2025-10-14T19:39:39.401Z", - "updatedAt": "2025-10-14T19:39:39.401Z" - } - }, - { - "id": "cmgqyuj6r0011mofwii4i80qf", - "error": { - "name": "ZodError", - "message": "[\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"key\"\n ],\n \"message\": \"Invalid input: expected string, received undefined\"\n },\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"name\"\n ],\n \"message\": \"Invalid input: expected string, received undefined\"\n },\n {\n \"expected\": \"number\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"tier\"\n ],\n \"message\": \"Invalid input: expected number, received undefined\"\n },\n {\n \"expected\": \"object\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"base\"\n ],\n \"message\": \"Invalid input: expected object, received undefined\"\n }\n]" - }, - "row": { - "id": "cmgqyuj6r0011mofwii4i80qf", - "key": "slime", - "name": "Slime", - "category": null, - "guildId": "1316592320954630144", - "stats": { - "attack": 6 - }, - "drops": null, - "metadata": null, - "createdAt": "2025-10-14T19:39:39.556Z", - "updatedAt": "2025-10-14T19:39:39.556Z" - } - }, - { - "id": "cmgqyuj8t0013mofwsc965xxk", - "error": { - "name": "ZodError", - "message": "[\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"key\"\n ],\n \"message\": \"Invalid input: expected string, received undefined\"\n },\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"name\"\n ],\n \"message\": \"Invalid input: expected string, received undefined\"\n },\n {\n \"expected\": \"number\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"tier\"\n ],\n \"message\": \"Invalid input: expected number, received undefined\"\n },\n {\n \"expected\": \"object\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"base\"\n ],\n \"message\": \"Invalid input: expected object, received undefined\"\n }\n]" - }, - "row": { - "id": "cmgqyuj8t0013mofwsc965xxk", - "key": "goblin", - "name": "Duende", - "category": null, - "guildId": "1316592320954630144", - "stats": { - "attack": 8 - }, - "drops": null, - "metadata": null, - "createdAt": "2025-10-14T19:39:39.629Z", - "updatedAt": "2025-10-14T19:39:39.629Z" - } - }, - { - "id": "cmgqyunde001zmofwyqw6oh49", - "error": { - "name": "ZodError", - "message": "[\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"key\"\n ],\n \"message\": \"Invalid input: expected string, received undefined\"\n },\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"name\"\n ],\n \"message\": \"Invalid input: expected string, received undefined\"\n },\n {\n \"expected\": \"number\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"tier\"\n ],\n \"message\": \"Invalid input: expected number, received undefined\"\n },\n {\n \"expected\": \"object\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"base\"\n ],\n \"message\": \"Invalid input: expected object, received undefined\"\n }\n]" - }, - "row": { - "id": "cmgqyunde001zmofwyqw6oh49", - "key": "orc", - "name": "Orco", - "category": null, - "guildId": "1316592320954630144", - "stats": { - "attack": 12, - "defense": 2 - }, - "drops": null, - "metadata": null, - "createdAt": "2025-10-14T19:39:44.978Z", - "updatedAt": "2025-10-14T19:39:44.978Z" - } - }, - { - "id": "cmgqyunf90021mofw2nb0j3ye", - "error": { - "name": "ZodError", - "message": "[\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"key\"\n ],\n \"message\": \"Invalid input: expected string, received undefined\"\n },\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"name\"\n ],\n \"message\": \"Invalid input: expected string, received undefined\"\n },\n {\n \"expected\": \"number\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"tier\"\n ],\n \"message\": \"Invalid input: expected number, received undefined\"\n },\n {\n \"expected\": \"object\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"base\"\n ],\n \"message\": \"Invalid input: expected object, received undefined\"\n }\n]" - }, - "row": { - "id": "cmgqyunf90021mofw2nb0j3ye", - "key": "troll", - "name": "Trol", - "category": null, - "guildId": "1316592320954630144", - "stats": { - "attack": 20, - "defense": 4 - }, - "drops": null, - "metadata": null, - "createdAt": "2025-10-14T19:39:45.045Z", - "updatedAt": "2025-10-14T19:39:45.045Z" - } - }, - { - "id": "cmgqyunha0023mofwyhh34uye", - "error": { - "name": "ZodError", - "message": "[\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"key\"\n ],\n \"message\": \"Invalid input: expected string, received undefined\"\n },\n {\n \"expected\": \"string\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"name\"\n ],\n \"message\": \"Invalid input: expected string, received undefined\"\n },\n {\n \"expected\": \"number\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"tier\"\n ],\n \"message\": \"Invalid input: expected number, received undefined\"\n },\n {\n \"expected\": \"object\",\n \"code\": \"invalid_type\",\n \"path\": [\n \"base\"\n ],\n \"message\": \"Invalid input: expected object, received undefined\"\n }\n]" - }, - "row": { - "id": "cmgqyunha0023mofwyhh34uye", - "key": "dragonling", - "name": "Dragoncito", - "category": null, - "guildId": "1316592320954630144", - "stats": { - "attack": 35, - "defense": 6 - }, - "drops": null, - "metadata": null, - "createdAt": "2025-10-14T19:39:45.119Z", - "updatedAt": "2025-10-14T19:39:45.119Z" - } - } -] \ No newline at end of file diff --git a/test/mob.test.ts b/test/mob.test.ts deleted file mode 100644 index 25b33d9..0000000 --- a/test/mob.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import assert from "assert"; -import { - computeMobStats, - getMobInstance, - MOB_DEFINITIONS, -} from "../src/game/mobs/mobData"; -import { createOrUpdateMob } from "../src/game/mobs/admin"; - -async function main() { - console.log("Starting formal mob tests..."); - - // Test computeMobStats deterministic - const def = MOB_DEFINITIONS[0]; - const s1 = computeMobStats(def, 1); - const s2 = computeMobStats(def, 2); - assert(s2.hp >= s1.hp, "HP should increase or stay with level"); - console.log("computeMobStats OK"); - - // Test getMobInstance - const inst = getMobInstance(def.key, 3); - assert(inst !== null, "getMobInstance should return an instance"); - assert( - typeof inst!.scaled.hp === "number", - "instance scaled.hp should be a number" - ); - console.log("getMobInstance OK"); - - // Test createOrUpdateMob in no-db mode (should not throw) - try { - const r = await createOrUpdateMob({ ...def, key: "test.unit.mob" } as any); - assert(r && r.def, "createOrUpdateMob must return def"); - console.log("createOrUpdateMob (no-db) OK"); - } catch (e) { - console.warn( - "createOrUpdateMob test skipped (DB needed):", - (e as any)?.message ?? e - ); - } - - console.log("All formal mob tests passed"); -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/test/scheduled_mob_attack_backup.json b/test/scheduled_mob_attack_backup.json deleted file mode 100644 index 30521d6..0000000 --- a/test/scheduled_mob_attack_backup.json +++ /dev/null @@ -1,42 +0,0 @@ -[ - { - "id": "cmgqyujes0014mofws1efy1p3", - "userId": "327207082203938818", - "guildId": "1316592320954630144", - "mobId": "cmgqyuj6r0011mofwii4i80qf", - "scheduleAt": "2025-10-14T19:39:44.844Z", - "processedAt": null, - "status": "scheduled", - "metadata": null - }, - { - "id": "cmgqyujes0015mofwyrgzfv6s", - "userId": "327207082203938818", - "guildId": "1316592320954630144", - "mobId": "cmgqyuj6r0011mofwii4i80qf", - "scheduleAt": "2025-10-14T19:39:54.844Z", - "processedAt": null, - "status": "scheduled", - "metadata": null - }, - { - "id": "cmgqyunn50024mofw3ae9ub5k", - "userId": "327207082203938818", - "guildId": "1316592320954630144", - "mobId": "cmgqyunde001zmofwyqw6oh49", - "scheduleAt": "2025-10-14T19:40:10.329Z", - "processedAt": null, - "status": "scheduled", - "metadata": null - }, - { - "id": "cmgqyunn50025mofwnj7xmke5", - "userId": "327207082203938818", - "guildId": "1316592320954630144", - "mobId": "cmgqyunha0023mofwyhh34uye", - "scheduleAt": "2025-10-14T19:40:25.329Z", - "processedAt": null, - "status": "scheduled", - "metadata": null - } -] \ No newline at end of file diff --git a/test/tmp_acorn.js b/test/tmp_acorn.js deleted file mode 100644 index ef1d788..0000000 --- a/test/tmp_acorn.js +++ /dev/null @@ -1,20 +0,0 @@ -const fs = require('fs'); const acorn = require('acorn'); -const path = '/home/shni/amayo/amayo/src/server/views/partials/dashboard/dashboard_items.ejs'; -const s = fs.readFileSync(path,'utf8'); -const m = s.match(/]*>([\s\S]*?)<\/script>/i); -if(!m){ console.error('NO_SCRIPT'); process.exit(2);} const src = m[1]; -try{ - acorn.parse(src, {ecmaVersion:2020}); - console.log('ACORN OK'); -}catch(err){ - console.error('ACORN ERR', err.message); - if(err.loc){ - const lines = src.split('\n'); - const L = err.loc.line; const C = err.loc.column; - console.error('at line', L, 'col', C); - const start = Math.max(0, L-4); const end = Math.min(lines.length, L+2); - for(let i=start;i' : ' ') + n.toString().padStart(4,' ') + '| ' + lines[i]); - } - } -} diff --git a/test/tmp_check.js b/test/tmp_check.js deleted file mode 100644 index 87dd31a..0000000 --- a/test/tmp_check.js +++ /dev/null @@ -1,43 +0,0 @@ -const fs = require('fs'); -const path = '/home/shni/amayo/amayo/src/server/views/partials/dashboard/dashboard_items.ejs'; -const s = fs.readFileSync(path,'utf8'); -const m = s.match(/]*>([\s\S]*?)<\/script>/i); -if(!m){ console.error('NO_SCRIPT'); process.exit(2); } -const src = m[1]; -console.log('SCRIPT LENGTH:', src.length); -let stack = []; -const opens = {'(':')','[':']','{':'}'}; -const closes = {')':'(',']':'[','}':'{'}; -let inSingle=false, inDouble=false, inTemplate=false, inComment=false, inLineComment=false, escape=false; -let lastSingle=-1, lastDouble=-1, lastTemplate=-1; -for(let i=0;i', src.slice(Math.max(0,lastSingle-60), lastSingle+60)); - if(inDouble) console.error('lastDouble@', lastDouble, 'context=>', src.slice(Math.max(0,lastDouble-60), lastDouble+60)); - if(inTemplate) console.error('lastTemplate@', lastTemplate, 'context=>', src.slice(Math.max(0,lastTemplate-60), lastTemplate+60)); -} -if(stack.length) console.error('UNMATCHED_OPEN', stack[stack.length-1], 'context=>', src.slice(Math.max(0,stack[stack.length-1].i-40), stack[stack.length-1].i+40)); -console.log('DONE'); -process.exit(0); diff --git a/test/tmp_find_parse_error.js b/test/tmp_find_parse_error.js deleted file mode 100644 index 85dc1c3..0000000 --- a/test/tmp_find_parse_error.js +++ /dev/null @@ -1,11 +0,0 @@ -const fs=require('fs'); const acorn=require('acorn'); -const path='/home/shni/amayo/amayo/src/server/views/partials/dashboard/dashboard_items.ejs'; -const s=fs.readFileSync(path,'utf8'); const m=s.match(/]*>([\s\S]*?)<\/script>/i); if(!m){ console.error('NO_SCRIPT'); process.exit(2);} const src=m[1]; -let low=0, high=src.length, bad=-1; -while(low]*>([\s\S]*?)<\/script>/i); if(!m){ console.error('NO_SCRIPT'); process.exit(2);} const src=m[1]; -const lines=src.split('\n'); -for(let i=1;i<=lines.length;i++){ - const chunk = lines.slice(0,i).join('\n'); - try{ acorn.parse(chunk,{ecmaVersion:2020}); } - catch(err){ console.error('FAIL at line', i, 'message', err.message); console.error('Error loc', err.loc); console.error('Context:'); const start=Math.max(1,i-5); const end=Math.min(lines.length, i+2); for(let j=start;j<=end;j++){ console.error((j===i? '>' : ' ')+j.toString().padStart(4,' ')+'| '+lines[j-1]); } process.exit(1); } -} -console.log('ALL LINES PARSED OK'); diff --git a/test/tmp_print_script.js b/test/tmp_print_script.js deleted file mode 100644 index eaef5ef..0000000 --- a/test/tmp_print_script.js +++ /dev/null @@ -1,11 +0,0 @@ -const fs = require('fs'); -const path = '/home/shni/amayo/amayo/src/server/views/partials/dashboard/dashboard_items.ejs'; -const s = fs.readFileSync(path,'utf8'); -const m = s.match(/]*>([\s\S]*?)<\/script>/i); -if(!m){ console.error('NO_SCRIPT'); process.exit(2); } -const src = m[1].replace(/\r\n/g,'\n'); -const lines = src.split('\n'); -for(let i=0;i]*>([\s\S]*?)<\/script>/i); if(!m){ console.error('NO_SCRIPT'); process.exit(2);} const src=m[1]; -const tok = acorn.tokenizer(src, {ecmaVersion:2020}); -let token; -const stack=[]; -while((token=tok.getToken()).type.label!="eof"){ - const lb = token.type.label; - if(lb==='(' || lb==='[' || lb==='{') stack.push({ch:lb, pos: token.start}); - if(lb===')' || lb===']' || lb==='}'){ - const expected = (lb===')'? '(' : (lb===']'? '[' : '{')); - if(stack.length===0){ console.error('UNMATCHED_CLOSE', lb, 'at', token.start); process.exit(3); } - const top = stack.pop(); - if(top.ch !== expected){ console.error('MISMATCH', top, 'vs', lb, 'at', token.start); process.exit(4); } - } -} -if(stack.length) console.error('UNMATCHED_OPEN', stack[stack.length-1]); else console.log('BALANCED'); diff --git a/test/tmp_token_trace.js b/test/tmp_token_trace.js deleted file mode 100644 index 4c5aef8..0000000 --- a/test/tmp_token_trace.js +++ /dev/null @@ -1,20 +0,0 @@ -const fs=require('fs'); const acorn=require('acorn'); -const path='/home/shni/amayo/amayo/src/server/views/partials/dashboard/dashboard_items.ejs'; -const s=fs.readFileSync(path,'utf8'); const m=s.match(/]*>([\s\S]*?)<\/script>/i); if(!m){ console.error('NO_SCRIPT'); process.exit(2);} const src=m[1]; -const tok = acorn.tokenizer(src, {ecmaVersion:2020}); -let t; const stack=[]; -while((t=tok.getToken()).type.label!=='eof'){ - const lb = t.type.label; - if(lb==='(' || lb==='[' || lb==='{'){ - stack.push({ch:lb,pos:t.start}); - if(t.start>2600 && t.start<3100) console.log('PUSH', lb, 'pos', t.start, 'stacklen', stack.length); - } - if(lb===')' || lb===']' || lb==='}'){ - const expected = (lb===')'? '(' : (lb===']'? '[' : '{')); - if(t.start>2600 && t.start<3100) console.log('POP', lb, 'pos', t.start, 'expect', expected, 'stacklen(before)', stack.length); - if(stack.length===0){ console.error('UNMATCHED_CLOSE', lb, 'at', t.start); process.exit(3); } - const top = stack.pop(); - if(top.ch !== expected){ console.error('MISMATCH', top, 'vs', lb, 'at', t.start); process.exit(4); } - } -} -console.log('FINISHED, stacklen', stack.length); diff --git a/test/tmp_tokens_inspect.js b/test/tmp_tokens_inspect.js deleted file mode 100644 index 518a7d3..0000000 --- a/test/tmp_tokens_inspect.js +++ /dev/null @@ -1,8 +0,0 @@ -const fs=require('fs'); const acorn=require('acorn'); -const path='/home/shni/amayo/amayo/src/server/views/partials/dashboard/dashboard_items.ejs'; -const s=fs.readFileSync(path,'utf8'); const m=s.match(/]*>([\s\S]*?)<\/script>/i); const src=m[1]; -const tok = acorn.tokenizer(src,{ecmaVersion:2020}); -let t; while((t=tok.getToken()).type.label!=='eof'){ - if(t.start>=2600 && t.start<=3050){ console.log(t.start, t.end, t.type.label, t.value); } -} -console.log('done'); diff --git a/test/unit/questsAchievements.unit.ts b/test/unit/questsAchievements.unit.ts deleted file mode 100644 index 7f3e4d7..0000000 --- a/test/unit/questsAchievements.unit.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { prisma } from "../../src/core/database/prisma"; -import { - generateDailyQuests, - updateQuestProgress, - claimQuestReward, - getPlayerQuests, -} from "../../src/game/quests/service"; -import { seedAchievements } from "../../src/game/achievements/seed"; -import { - checkAchievements, - getPlayerAchievements, -} from "../../src/game/achievements/service"; -import { - ensureGuildExists, - ensureUserExists, -} from "../../src/game/core/userService"; - -async function resetDb(guildId: string) { - // Delete created quests, achievements and progress for a clean test - await prisma.questProgress.deleteMany({ where: { guildId } }).catch(() => {}); - await prisma.quest.deleteMany({ where: { guildId } }).catch(() => {}); - await prisma.playerAchievement - .deleteMany({ where: { guildId } }) - .catch(() => {}); - await prisma.achievement.deleteMany({ where: { guildId } }).catch(() => {}); -} - -async function runTests() { - const guildId = "test-guild-1"; - const userId = "user-test-1"; - - // Make sure guild and user exist to satisfy FK constraints - await ensureGuildExists(guildId, "Test Guild"); - await ensureUserExists(userId); - - await resetDb(guildId); - - console.log("Seeding achievements..."); - await seedAchievements(guildId); - const achBefore = await getPlayerAchievements(userId, guildId); - console.log("Player achievements before:", achBefore); - - console.log("Generating daily quests..."); - const count = await generateDailyQuests(guildId); - console.log("Daily quests generated:", count); - - const quests = await getPlayerQuests(userId, guildId); - console.log( - "Player quests after generate:", - Object.keys(quests).reduce((acc, k) => acc + (quests as any)[k].length, 0) - ); - - // Pick a quest from mining if any - const daily = quests.daily as any[]; - if (daily.length > 0) { - const q = daily[0].quest; - console.log("Testing progress update for quest:", q.key); - const updates = await updateQuestProgress( - userId, - guildId, - (q.requirements as any).type, - (q.requirements as any).count - ); - console.log( - "Quests completed by updateQuestProgress:", - updates.map((u) => u.key) - ); - - // Claim reward - const progressRows = await prisma.questProgress.findMany({ - where: { userId, guildId, questId: q.id }, - }); - if (progressRows.length > 0) { - const res = await claimQuestReward(userId, guildId, q.id).catch((e) => { - console.error("Claim failed", e); - return null; - }); - console.log("Claim result:", res); - } - } else { - console.warn("No daily quests found in test run — skipping claim flow"); - } - - // Trigger achievements check by running checkAchievements with a trigger from seeded achievements - console.log("Checking achievements via trigger mine_count"); - const unlocked = await checkAchievements(userId, guildId, "mine_count"); - console.log( - "Achievements unlocked:", - unlocked.map((a) => a.key) - ); - - const achAfter = await getPlayerAchievements(userId, guildId); - console.log("Player achievements after:", achAfter); - - console.log("Tests finished."); -} - -runTests() - .then(() => process.exit(0)) - .catch((e) => { - console.error(e); - process.exit(1); - }); diff --git a/test/unit/rewardMods.test.ts b/test/unit/rewardMods.test.ts deleted file mode 100644 index b2f108f..0000000 --- a/test/unit/rewardMods.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { test } from "uvu"; -import * as assert from "uvu/assert"; -import { pickDropFromDef } from "../../src/game/minigames/testHelpers"; - -// deterministic randomness helper -function seedRandom(seed: number) { - let s = seed % 2147483647; - if (s <= 0) s += 2147483646; - return () => (s = (s * 16807) % 2147483647) / 2147483647; -} - -// Patch Math.random for deterministic tests -const realRandom = Math.random; - -test.before(() => { - (Math as any).random = seedRandom(42) as any; -}); - -test.after(() => { - (Math as any).random = realRandom; -}); - -test("pickDropFromDef chooses weighted item", () => { - const def = { - drops: [ - { itemKey: "ore.iron", qty: 1, weight: 8 }, - { itemKey: "ore.gold", qty: 1, weight: 2 }, - ], - } as any; - const picks = new Set(); - for (let i = 0; i < 10; i++) { - const p = pickDropFromDef(def); - assert.ok(p && (p.itemKey === "ore.iron" || p.itemKey === "ore.gold")); - picks.add(p!.itemKey); - } - // with seeded RNG both options should appear - assert.ok(picks.size >= 1); -}); - -test("pickDropFromDef chooses from map", () => { - const def = { drops: { "ore.iron": 1, "ore.gold": 2 } } as any; - const p = pickDropFromDef(def); - assert.ok(p && (p.itemKey === "ore.iron" || p.itemKey === "ore.gold")); -}); - -// coinMultiplier behavior is multiplicative in current design; test small scenario -test("coin multiplier aggregation (product)", () => { - const mobs = [ - { rewardMods: { coinMultiplier: 1.1 } }, - { rewardMods: { coinMultiplier: 1.2 } }, - ]; - const product = mobs.reduce( - (acc, m) => acc * ((m.rewardMods?.coinMultiplier as number) || 1), - 1 - ); - assert.equal(Math.round(product * 100) / 100, Math.round(1.32 * 100) / 100); -}); - -test.run(); diff --git a/test/unit/rewardMods.unit.ts b/test/unit/rewardMods.unit.ts deleted file mode 100644 index 4306a38..0000000 --- a/test/unit/rewardMods.unit.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as assert from "assert"; -import { pickDropFromDef } from "../../src/game/minigames/testHelpers"; - -// deterministic RNG -function seedRandom(seed: number) { - let s = seed % 2147483647; - if (s <= 0) s += 2147483646; - return () => (s = (s * 16807) % 2147483647) / 2147483647; -} - -const rand = seedRandom(42); -const realRandom = Math.random; -(Math as any).random = rand; - -try { - // weighted - const def1 = { - drops: [ - { itemKey: "ore.iron", qty: 1, weight: 8 }, - { itemKey: "ore.gold", qty: 1, weight: 2 }, - ], - } as any; - const picks = new Set(); - for (let i = 0; i < 20; i++) { - const p = pickDropFromDef(def1); - if (!p) throw new Error("expected pick"); - picks.add(p.itemKey); - } - assert.ok(picks.size >= 1, "expected at least 1 picked key"); - - // map - const def2 = { drops: { "ore.iron": 1, "ore.gold": 2 } } as any; - const p2 = pickDropFromDef(def2); - assert.ok(p2 && (p2.itemKey === "ore.iron" || p2.itemKey === "ore.gold")); - - // coin multiplier product - const mobs = [ - { rewardMods: { coinMultiplier: 1.1 } }, - { rewardMods: { coinMultiplier: 1.2 } }, - ]; - const product = mobs.reduce( - (acc, m) => acc * ((m.rewardMods?.coinMultiplier as number) || 1), - 1 - ); - assert.strictEqual( - Math.round(product * 100) / 100, - Math.round(1.32 * 100) / 100 - ); - - console.log("All unit tests passed"); - (Math as any).random = realRandom; - process.exit(0); -} catch (e) { - (Math as any).random = realRandom; - console.error(e); - process.exit(1); -}