@omnizap-system/omnizap 2.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/.clusterfuzzlite/Dockerfile +10 -0
  2. package/.env.example +907 -0
  3. package/.github/codeql/codeql-config.yml +10 -0
  4. package/.github/dependabot.yml +35 -0
  5. package/.github/workflows/ci.yml +73 -0
  6. package/.github/workflows/codeql.yml +106 -0
  7. package/.github/workflows/db-migration-check.yml +98 -0
  8. package/.github/workflows/dependency-review.yml +22 -0
  9. package/.github/workflows/deploy.yml +95 -0
  10. package/.github/workflows/release.yml +106 -0
  11. package/.github/workflows/security-attest-provenance.yml +51 -0
  12. package/.github/workflows/security-gitleaks.yml +34 -0
  13. package/.github/workflows/security-runner-hardening.yml +31 -0
  14. package/.github/workflows/security-scorecard.yml +44 -0
  15. package/.github/workflows/security-zap-baseline.yml +44 -0
  16. package/.github/workflows/security-zap-full-scan.yml +43 -0
  17. package/.github/workflows/security-zizmor.yml +36 -0
  18. package/.github/workflows/wiki-sync.yml +44 -0
  19. package/.gitleaks.toml +15 -0
  20. package/.prettierrc +34 -0
  21. package/CODE_OF_CONDUCT.md +114 -0
  22. package/LICENSE +56 -0
  23. package/README.md +110 -0
  24. package/SECURITY.md +110 -0
  25. package/app/config/index.js +4 -0
  26. package/app/configParts/adminIdentity.js +92 -0
  27. package/app/configParts/baileysConfig.js +1818 -0
  28. package/app/configParts/groupUtils.js +692 -0
  29. package/app/configParts/loggerConfig.js +394 -0
  30. package/app/configParts/messagePersistenceService.js +305 -0
  31. package/app/connection/baileysCompatibility.test.js +40 -0
  32. package/app/connection/baileysDbAuthState.js +344 -0
  33. package/app/connection/socketController.js +2243 -0
  34. package/app/controllers/messageController.js +7 -0
  35. package/app/controllers/messagePipeline/commandMiddleware.js +146 -0
  36. package/app/controllers/messagePipeline/conversationMiddleware.js +183 -0
  37. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +522 -0
  38. package/app/controllers/messagePipeline/postProcessingMiddleware.js +41 -0
  39. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +166 -0
  40. package/app/controllers/messageProcessingPipeline.js +699 -0
  41. package/app/modules/adminModule/AGENT.md +4056 -0
  42. package/app/modules/adminModule/adminAiHelpService.js +56 -0
  43. package/app/modules/adminModule/adminConfigRuntime.js +177 -0
  44. package/app/modules/adminModule/commandConfig.json +7122 -0
  45. package/app/modules/adminModule/groupCommandHandlers.js +1823 -0
  46. package/app/modules/adminModule/groupCommandHandlers.test.js +350 -0
  47. package/app/modules/adminModule/groupEventHandlers.js +399 -0
  48. package/app/modules/aiModule/AGENT.md +547 -0
  49. package/app/modules/aiModule/aiAiHelpService.js +14 -0
  50. package/app/modules/aiModule/aiConfigRuntime.js +135 -0
  51. package/app/modules/aiModule/catCommand.js +967 -0
  52. package/app/modules/aiModule/commandConfig.json +981 -0
  53. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
  54. package/app/modules/gameModule/AGENT.md +196 -0
  55. package/app/modules/gameModule/commandConfig.json +366 -0
  56. package/app/modules/gameModule/diceCommand.js +42 -0
  57. package/app/modules/gameModule/gameAiHelpService.js +14 -0
  58. package/app/modules/gameModule/gameConfigRuntime.js +68 -0
  59. package/app/modules/menuModule/AGENT.md +205 -0
  60. package/app/modules/menuModule/commandConfig.json +366 -0
  61. package/app/modules/menuModule/common.js +316 -0
  62. package/app/modules/menuModule/menuAiHelpService.js +14 -0
  63. package/app/modules/menuModule/menuConfigRuntime.js +68 -0
  64. package/app/modules/menuModule/menus.js +66 -0
  65. package/app/modules/playModule/AGENT.md +321 -0
  66. package/app/modules/playModule/commandConfig.json +584 -0
  67. package/app/modules/playModule/playAiHelpService.js +14 -0
  68. package/app/modules/playModule/playCommand.js +1417 -0
  69. package/app/modules/playModule/playConfigRuntime.js +68 -0
  70. package/app/modules/quoteModule/AGENT.md +199 -0
  71. package/app/modules/quoteModule/commandConfig.json +366 -0
  72. package/app/modules/quoteModule/quoteAiHelpService.js +14 -0
  73. package/app/modules/quoteModule/quoteCommand.js +842 -0
  74. package/app/modules/quoteModule/quoteConfigRuntime.js +68 -0
  75. package/app/modules/rpgPokemonModule/AGENT.md +229 -0
  76. package/app/modules/rpgPokemonModule/commandConfig.json +386 -0
  77. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +795 -0
  78. package/app/modules/rpgPokemonModule/rpgBattleService.js +2110 -0
  79. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +770 -0
  80. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  81. package/app/modules/rpgPokemonModule/rpgPokemonAiHelpService.js +14 -0
  82. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +174 -0
  83. package/app/modules/rpgPokemonModule/rpgPokemonConfigRuntime.js +68 -0
  84. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  85. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  86. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  87. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  88. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1847 -0
  89. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6839 -0
  90. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  91. package/app/modules/statsModule/AGENT.md +320 -0
  92. package/app/modules/statsModule/commandConfig.json +540 -0
  93. package/app/modules/statsModule/globalRankingCommand.js +64 -0
  94. package/app/modules/statsModule/rankingCommand.js +41 -0
  95. package/app/modules/statsModule/rankingCommon.js +1305 -0
  96. package/app/modules/statsModule/statsAiHelpService.js +14 -0
  97. package/app/modules/statsModule/statsConfigRuntime.js +68 -0
  98. package/app/modules/stickerModule/AGENT.md +692 -0
  99. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  100. package/app/modules/stickerModule/commandConfig.json +1216 -0
  101. package/app/modules/stickerModule/convertToWebp.js +367 -0
  102. package/app/modules/stickerModule/stickerAiHelpService.js +14 -0
  103. package/app/modules/stickerModule/stickerCommand.js +446 -0
  104. package/app/modules/stickerModule/stickerConfigRuntime.js +68 -0
  105. package/app/modules/stickerModule/stickerConvertCommand.js +159 -0
  106. package/app/modules/stickerModule/stickerTextCommand.js +653 -0
  107. package/app/modules/stickerPackModule/AGENT.md +215 -0
  108. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  109. package/app/modules/stickerPackModule/autoPackCollectorService.js +357 -0
  110. package/app/modules/stickerPackModule/commandConfig.json +387 -0
  111. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +227 -0
  112. package/app/modules/stickerPackModule/domainEvents.js +52 -0
  113. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +429 -0
  114. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +75 -0
  115. package/app/modules/stickerPackModule/semanticThemeClusterService.js +544 -0
  116. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +400 -0
  117. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  118. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +175 -0
  119. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +3702 -0
  120. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +559 -0
  121. package/app/modules/stickerPackModule/stickerClassificationService.js +557 -0
  122. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +249 -0
  123. package/app/modules/stickerPackModule/stickerDomainEventBus.js +65 -0
  124. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +208 -0
  125. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +99 -0
  126. package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
  127. package/app/modules/stickerPackModule/stickerPackAiHelpService.js +14 -0
  128. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1148 -0
  129. package/app/modules/stickerPackModule/stickerPackConfigRuntime.js +68 -0
  130. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +152 -0
  131. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  132. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +101 -0
  133. package/app/modules/stickerPackModule/stickerPackItemRepository.js +432 -0
  134. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +313 -0
  135. package/app/modules/stickerPackModule/stickerPackMessageService.js +268 -0
  136. package/app/modules/stickerPackModule/stickerPackRepository.js +450 -0
  137. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +179 -0
  138. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +271 -0
  139. package/app/modules/stickerPackModule/stickerPackService.js +733 -0
  140. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +32 -0
  141. package/app/modules/stickerPackModule/stickerPackUtils.js +107 -0
  142. package/app/modules/stickerPackModule/stickerStorageService.js +559 -0
  143. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +242 -0
  144. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +242 -0
  145. package/app/modules/systemMetricsModule/AGENT.md +193 -0
  146. package/app/modules/systemMetricsModule/commandConfig.json +344 -0
  147. package/app/modules/systemMetricsModule/pingCommand.js +399 -0
  148. package/app/modules/systemMetricsModule/systemMetricsAiHelpService.js +14 -0
  149. package/app/modules/systemMetricsModule/systemMetricsConfigRuntime.js +68 -0
  150. package/app/modules/tiktokModule/AGENT.md +196 -0
  151. package/app/modules/tiktokModule/commandConfig.json +366 -0
  152. package/app/modules/tiktokModule/tiktokAiHelpService.js +14 -0
  153. package/app/modules/tiktokModule/tiktokCommand.js +716 -0
  154. package/app/modules/tiktokModule/tiktokConfigRuntime.js +68 -0
  155. package/app/modules/userModule/AGENT.md +200 -0
  156. package/app/modules/userModule/commandConfig.json +386 -0
  157. package/app/modules/userModule/userAiHelpService.js +14 -0
  158. package/app/modules/userModule/userCommand.js +1155 -0
  159. package/app/modules/userModule/userConfigRuntime.js +68 -0
  160. package/app/modules/waifuPicsModule/AGENT.md +431 -0
  161. package/app/modules/waifuPicsModule/commandConfig.json +780 -0
  162. package/app/modules/waifuPicsModule/waifuPicsAiHelpService.js +14 -0
  163. package/app/modules/waifuPicsModule/waifuPicsCommand.js +586 -0
  164. package/app/modules/waifuPicsModule/waifuPicsConfigRuntime.js +68 -0
  165. package/app/observability/metrics.js +766 -0
  166. package/app/services/ai/aiHelpResponseCacheRepository.js +280 -0
  167. package/app/services/ai/aiLearningRepository.js +400 -0
  168. package/app/services/ai/commandConfigEnrichmentRepository.js +769 -0
  169. package/app/services/ai/commandConfigEnrichmentService.js +452 -0
  170. package/app/services/ai/commandConfigValidationService.js +443 -0
  171. package/app/services/ai/commandToolBuilderService.js +192 -0
  172. package/app/services/ai/conversationRouterService.js +516 -0
  173. package/app/services/ai/geminiService.js +115 -0
  174. package/app/services/ai/geminiService.test.js +87 -0
  175. package/app/services/ai/globalModuleAiHelpService.js +1412 -0
  176. package/app/services/ai/globalToolCallingService.js +203 -0
  177. package/app/services/ai/messageCommandExecutionService.js +391 -0
  178. package/app/services/ai/moduleAiHelpCoreService.js +1099 -0
  179. package/app/services/ai/moduleAiHelpWrapperFactory.js +65 -0
  180. package/app/services/ai/moduleCommandConfigRuntimeService.js +113 -0
  181. package/app/services/ai/moduleToolExecutorService.js +464 -0
  182. package/app/services/ai/moduleToolRegistryService.js +178 -0
  183. package/app/services/ai/toolCandidateSelectorService.js +781 -0
  184. package/app/services/auth/googleWebLinkService.js +80 -0
  185. package/app/services/auth/whatsappLoginLinkService.js +230 -0
  186. package/app/services/external/pokeApiService.js +398 -0
  187. package/app/services/group/groupMetadataService.js +311 -0
  188. package/app/services/infra/dbWriteQueue.js +874 -0
  189. package/app/services/infra/featureFlagService.js +131 -0
  190. package/app/services/infra/queueUtils.js +55 -0
  191. package/app/services/messaging/captchaService.js +491 -0
  192. package/app/services/messaging/messagePersistenceService.js +1 -0
  193. package/app/services/messaging/newsBroadcastService.js +347 -0
  194. package/app/services/sticker/stickerFocusService.js +347 -0
  195. package/app/services/sticker/stickerFocusService.test.js +43 -0
  196. package/app/store/aiPromptStore.js +38 -0
  197. package/app/store/conversationSessionStore.js +131 -0
  198. package/app/store/groupConfigStore.js +58 -0
  199. package/app/store/premiumUserStore.js +54 -0
  200. package/app/utils/antiLink/antiLinkModule.js +700 -0
  201. package/app/utils/http/getImageBufferModule.js +18 -0
  202. package/app/utils/json/jsonSanitizer.js +113 -0
  203. package/app/utils/json/jsonSanitizer.test.js +40 -0
  204. package/app/utils/systemMetrics/systemMetricsModule.js +88 -0
  205. package/app/workers/aiLearningWorker.js +605 -0
  206. package/app/workers/commandConfigEnrichmentWorker.js +242 -0
  207. package/database/index.js +2075 -0
  208. package/database/init.js +151 -0
  209. package/database/migrations/.gitkeep +0 -0
  210. package/database/migrations/20260307_d0_hardening_down.sql +64 -0
  211. package/database/migrations/20260307_d0_hardening_up.sql +79 -0
  212. package/database/migrations/20260307_d1_terms_acceptance_down.sql +11 -0
  213. package/database/migrations/20260307_d1_terms_acceptance_up.sql +37 -0
  214. package/database/migrations/20260307_d2_auth_hardening_down.sql +75 -0
  215. package/database/migrations/20260307_d2_auth_hardening_up.sql +100 -0
  216. package/database/migrations/20260314_d7_canonical_sender_down.sql +53 -0
  217. package/database/migrations/20260314_d7_canonical_sender_up.sql +114 -0
  218. package/database/migrations/20260406_d30_security_analytics_down.sql +95 -0
  219. package/database/migrations/20260406_d30_security_analytics_up.sql +292 -0
  220. package/database/migrations/20260407_d31_web_google_session_token_hardening_down.sql +2 -0
  221. package/database/migrations/20260407_d31_web_google_session_token_hardening_up.sql +17 -0
  222. package/database/migrations/20260408_d32_ai_help_response_cache_down.sql +1 -0
  223. package/database/migrations/20260408_d32_ai_help_response_cache_up.sql +22 -0
  224. package/database/migrations/20260409_d33_ai_learning_tables_down.sql +4 -0
  225. package/database/migrations/20260409_d33_ai_learning_tables_up.sql +52 -0
  226. package/database/migrations/20260410_d34_command_config_enrichment_down.sql +3 -0
  227. package/database/migrations/20260410_d34_command_config_enrichment_up.sql +48 -0
  228. package/database/schema.sql +1186 -0
  229. package/docker-compose.yml +104 -0
  230. package/docs/audits/stickerCatalogController-out-of-scope.md +103 -0
  231. package/docs/audits/stickerCatalogController-symbols.md +58 -0
  232. package/docs/compliance/acceptable-use-policy-2026-03-07.md +35 -0
  233. package/docs/compliance/dpa-b2b-standard-2026-03-07.md +80 -0
  234. package/docs/compliance/monthly-compliance-checklist-2026-03-07.md +88 -0
  235. package/docs/compliance/notice-and-takedown-policy-2026-03-07.md +34 -0
  236. package/docs/compliance/privacy-policy-2026-03-07.md +75 -0
  237. package/docs/compliance/subprocessors-inventory-2026-03-07.md +16 -0
  238. package/docs/database/production-db-evolution-runbook-2026q1.md +365 -0
  239. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +86 -0
  240. package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +77 -0
  241. package/docs/security/network-hardening-runbook-2026-03-07.md +137 -0
  242. package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +238 -0
  243. package/docs/seo/satellite-page-template.md +116 -0
  244. package/docs/seo/satellite-pages-phase1.json +364 -0
  245. package/docs/wiki/Home.md +120 -0
  246. package/docs/wiki/pair-extraordinaire-2026-03-08.md +3 -0
  247. package/docs/wiki/recent-changes-2026-03-08.md +47 -0
  248. package/ecosystem.prod.config.cjs +135 -0
  249. package/eslint.config.js +89 -0
  250. package/index.js +488 -0
  251. package/ml/clip_classifier/Dockerfile +18 -0
  252. package/ml/clip_classifier/README.md +118 -0
  253. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  254. package/ml/clip_classifier/classifier.py +654 -0
  255. package/ml/clip_classifier/embedding_store.py +481 -0
  256. package/ml/clip_classifier/env_loader.py +15 -0
  257. package/ml/clip_classifier/llm_label_expander.py +144 -0
  258. package/ml/clip_classifier/main.py +213 -0
  259. package/ml/clip_classifier/requirements.txt +10 -0
  260. package/ml/clip_classifier/similarity_engine.py +74 -0
  261. package/new-logo.png +0 -0
  262. package/observability/alert-rules.yml +60 -0
  263. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  264. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  265. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  266. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  267. package/observability/loki-config.yml +38 -0
  268. package/observability/mysql-setup.sql +46 -0
  269. package/observability/prometheus.yml +35 -0
  270. package/observability/promtail-config.yml +84 -0
  271. package/observability/sticker-catalog-slo.md +83 -0
  272. package/observability/sticker-scale-hardening-rollout.md +128 -0
  273. package/package.json +144 -0
  274. package/public/apple-touch-icon.png +0 -0
  275. package/public/assets/css/commands-react.input.css +71 -0
  276. package/public/assets/css/create-pack-react.input.css +31 -0
  277. package/public/assets/css/home-react.input.css +106 -0
  278. package/public/assets/css/login-react.input.css +58 -0
  279. package/public/assets/css/stickers-react.input.css +18 -0
  280. package/public/assets/css/terms-react.input.css +115 -0
  281. package/public/assets/css/user-react.input.css +57 -0
  282. package/public/assets/images/brand-icon-192.png +0 -0
  283. package/public/assets/images/brand-logo-128.webp +0 -0
  284. package/public/assets/images/hero-banner-1280.jpg +0 -0
  285. package/public/comandos/commands-catalog.json +4517 -0
  286. package/public/css/api-docs.css +161 -0
  287. package/public/css/stickers-admin.css +1288 -0
  288. package/public/css/styles.css +679 -0
  289. package/public/css/systemadm/admin.css +474 -0
  290. package/public/css/systemadm/base.css +73 -0
  291. package/public/css/systemadm/components.css +662 -0
  292. package/public/css/systemadm/layout.css +229 -0
  293. package/public/css/systemadm/tokens.css +56 -0
  294. package/public/favicon-16x16.png +0 -0
  295. package/public/favicon-32x32.png +0 -0
  296. package/public/favicon.ico +0 -0
  297. package/public/js/apps/apiDocsApp.js +235 -0
  298. package/public/js/apps/commandsReactApp.js +528 -0
  299. package/public/js/apps/createPackApp.js +1646 -0
  300. package/public/js/apps/homeReactApp.js +942 -0
  301. package/public/js/apps/loginReactApp.js +496 -0
  302. package/public/js/apps/stickersAdminApp.js +1753 -0
  303. package/public/js/apps/stickersApp.js +3797 -0
  304. package/public/js/apps/termsReactApp.js +528 -0
  305. package/public/js/apps/userApp.js +2540 -0
  306. package/public/js/apps/userProfile/actions.js +66 -0
  307. package/public/js/apps/userReactApp.js +547 -0
  308. package/public/js/catalog.js +950 -0
  309. package/public/pages/api-docs.html +40 -0
  310. package/public/pages/aup.html +158 -0
  311. package/public/pages/comandos.html +41 -0
  312. package/public/pages/dpa.html +227 -0
  313. package/public/pages/home.html +45 -0
  314. package/public/pages/licenca.html +182 -0
  315. package/public/pages/login.html +40 -0
  316. package/public/pages/notice-and-takedown.html +234 -0
  317. package/public/pages/politica-de-privacidade.html +251 -0
  318. package/public/pages/seo-bot-whatsapp-para-grupo.html +350 -0
  319. package/public/pages/seo-bot-whatsapp-sem-programar.html +350 -0
  320. package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +350 -0
  321. package/public/pages/seo-como-criar-comandos-whatsapp.html +350 -0
  322. package/public/pages/seo-como-evitar-spam-no-whatsapp.html +350 -0
  323. package/public/pages/seo-como-moderar-grupo-whatsapp.html +350 -0
  324. package/public/pages/seo-como-organizar-comunidade-whatsapp.html +350 -0
  325. package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +350 -0
  326. package/public/pages/stickers-admin.html +31 -0
  327. package/public/pages/stickers-create.html +41 -0
  328. package/public/pages/stickers.html +45 -0
  329. package/public/pages/suboperadores.html +237 -0
  330. package/public/pages/termos-de-uso-texto-integral.html +241 -0
  331. package/public/pages/termos-de-uso.html +41 -0
  332. package/public/pages/user-password-reset.html +32 -0
  333. package/public/pages/user-systemadm.html +508 -0
  334. package/public/pages/user.html +39 -0
  335. package/public/robots.txt +9 -0
  336. package/public/site.webmanifest +24 -0
  337. package/public/sitemap.xml +98 -0
  338. package/schemas/command-config.schema.json +582 -0
  339. package/scripts/baileys-compat-smoke.mjs +12 -0
  340. package/scripts/cache-bust.mjs +142 -0
  341. package/scripts/deploy.sh +916 -0
  342. package/scripts/email-broadcast-terms-update.mjs +170 -0
  343. package/scripts/enrich-command-discovery-fields.mjs +286 -0
  344. package/scripts/generate-command-config-schema.mjs +273 -0
  345. package/scripts/generate-commands-catalog.mjs +308 -0
  346. package/scripts/generate-module-agents.mjs +631 -0
  347. package/scripts/generate-seo-satellite-pages.mjs +400 -0
  348. package/scripts/github-deploy-notify.mjs +174 -0
  349. package/scripts/github-release-notify.mjs +219 -0
  350. package/scripts/release.sh +599 -0
  351. package/scripts/run-codeql-local.sh +116 -0
  352. package/scripts/run-prettier-all.mjs +25 -0
  353. package/scripts/security-smoketest.mjs +581 -0
  354. package/scripts/sticker-catalog-loadtest.mjs +210 -0
  355. package/scripts/sticker-worker-task.mjs +119 -0
  356. package/scripts/sync-readme-snapshot.mjs +133 -0
  357. package/scripts/validate-command-config-schema.mjs +130 -0
  358. package/scripts/validate-command-configs.mjs +15 -0
  359. package/scripts/wiki-sync.sh +191 -0
  360. package/server/auth/googleWebAuth/googleWebAuthRuntime.js +62 -0
  361. package/server/auth/googleWebAuth/googleWebAuthService.js +807 -0
  362. package/server/auth/jwt/webJwtService.js +147 -0
  363. package/server/auth/stickerCatalogAuthContext.js +165 -0
  364. package/server/auth/termsAcceptance/termsAcceptanceHandler.js +189 -0
  365. package/server/auth/userPassword/index.js +14 -0
  366. package/server/auth/userPassword/userPasswordAuthService.js +422 -0
  367. package/server/auth/userPassword/userPasswordCrypto.js +199 -0
  368. package/server/auth/userPassword/userPasswordCrypto.test.js +76 -0
  369. package/server/auth/userPassword/userPasswordRecoveryService.js +728 -0
  370. package/server/auth/validation/authSchemas.js +236 -0
  371. package/server/auth/webAccount/webAccountHandlers.js +1434 -0
  372. package/server/controllers/admin/adminBanService.js +138 -0
  373. package/server/controllers/admin/adminPanelHandlers.js +2083 -0
  374. package/server/controllers/admin/stickerCatalogAdminContext.js +17 -0
  375. package/server/controllers/admin/systemAdminController.js +201 -0
  376. package/server/controllers/email/emailAutomationController.js +239 -0
  377. package/server/controllers/metricsController.js +21 -0
  378. package/server/controllers/seo/stickerCatalogSeoContext.js +514 -0
  379. package/server/controllers/sticker/nonCatalogHandlers.js +303 -0
  380. package/server/controllers/sticker/stickerCatalogController.js +4700 -0
  381. package/server/controllers/system/contactController.js +115 -0
  382. package/server/controllers/system/githubController.js +137 -0
  383. package/server/controllers/system/stickerCatalogSystemContext.js +758 -0
  384. package/server/controllers/system/storageController.js +154 -0
  385. package/server/controllers/system/systemController.js +135 -0
  386. package/server/controllers/system/systemMetricsController.js +156 -0
  387. package/server/controllers/system/visitController.js +90 -0
  388. package/server/controllers/userController.js +145 -0
  389. package/server/email/emailAutomationRuntime.js +225 -0
  390. package/server/email/emailAutomationService.js +125 -0
  391. package/server/email/emailOutboxRepository.js +282 -0
  392. package/server/email/emailTemplateService.js +480 -0
  393. package/server/email/emailTransportService.js +156 -0
  394. package/server/http/clientIp.js +95 -0
  395. package/server/http/httpRequestUtils.js +262 -0
  396. package/server/http/httpRequestUtils.test.js +80 -0
  397. package/server/http/httpServer.js +180 -0
  398. package/server/http/requestContext.js +20 -0
  399. package/server/http/siteRoutingUtils.js +87 -0
  400. package/server/index.js +1 -0
  401. package/server/middleware/cachePolicy.js +26 -0
  402. package/server/middleware/cachePolicyHelpers.js +1 -0
  403. package/server/middleware/endpointRateLimit.js +181 -0
  404. package/server/middleware/rateLimit.js +70 -0
  405. package/server/middleware/requireAdminAuth.js +48 -0
  406. package/server/middleware/securityHeaders.js +97 -0
  407. package/server/routes/admin/systemAdminRouter.js +64 -0
  408. package/server/routes/email/emailAutomationRouter.js +46 -0
  409. package/server/routes/health/healthRouter.js +41 -0
  410. package/server/routes/indexRouter.js +234 -0
  411. package/server/routes/metrics/metricsRouter.js +58 -0
  412. package/server/routes/static/staticPageRouter.js +134 -0
  413. package/server/routes/sticker/catalogHandlers/catalogAdminHttp.js +105 -0
  414. package/server/routes/sticker/catalogHandlers/catalogAuthHttp.js +77 -0
  415. package/server/routes/sticker/catalogHandlers/catalogPublicHttp.js +120 -0
  416. package/server/routes/sticker/catalogHandlers/catalogUploadHttp.js +83 -0
  417. package/server/routes/sticker/catalogRouter.js +77 -0
  418. package/server/routes/sticker/stickerApiRouter.js +84 -0
  419. package/server/routes/sticker/stickerDataRouter.js +145 -0
  420. package/server/routes/sticker/stickerSiteRouter.js +43 -0
  421. package/server/routes/user/userApiPaths.js +66 -0
  422. package/server/routes/user/userRouter.js +65 -0
  423. package/server/utils/safePath.js +26 -0
  424. package/utils/logger/loggerModule.js +35 -0
  425. package/vite.config.mjs +38 -0
@@ -0,0 +1,770 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { createWildEncounter, resolveBattleTurn, resolveCaptureAttempt, resolveEvolutionByItem, resolveEvolutionByLevel, resolveSingleAttack } from './rpgBattleService.js';
5
+
6
+ const PREFERRED_MOVE_NAMES = ['tackle', 'quick-attack', 'scratch', 'pound', 'ember', 'water-gun', 'vine-whip', 'bite', 'gust', 'swift', 'struggle'];
7
+
8
+ const ensurePokeApiCache = () => {
9
+ if (!(globalThis.__omnizapPokeApiCache instanceof Map)) {
10
+ globalThis.__omnizapPokeApiCache = new Map();
11
+ }
12
+ return globalThis.__omnizapPokeApiCache;
13
+ };
14
+
15
+ const setCache = (cache, key, data) => {
16
+ cache.set(key, {
17
+ data,
18
+ expiresAt: Date.now() + 60 * 60 * 1000,
19
+ });
20
+ };
21
+
22
+ const buildTypeData = ({ pokemonIds = [], doubleDamageTo = [], halfDamageTo = [], noDamageTo = [] } = {}) => ({
23
+ damage_relations: {
24
+ double_damage_to: doubleDamageTo.map((name) => ({ name })),
25
+ half_damage_to: halfDamageTo.map((name) => ({ name })),
26
+ no_damage_to: noDamageTo.map((name) => ({ name })),
27
+ },
28
+ pokemon: pokemonIds.map((id) => ({
29
+ pokemon: {
30
+ name: `poke-${id}`,
31
+ url: `https://pokeapi.co/api/v2/pokemon/${id}/`,
32
+ },
33
+ })),
34
+ });
35
+
36
+ const buildMoveData = (name, type = 'normal', power = 40) => ({
37
+ id: Math.max(1, name.length * 17),
38
+ name,
39
+ power,
40
+ accuracy: 100,
41
+ pp: 35,
42
+ damage_class: { name: power > 0 ? 'physical' : 'status' },
43
+ type: { name: type },
44
+ });
45
+
46
+ const buildPokemonData = ({ id, name, speciesId = id, primaryType = 'normal', moves = PREFERRED_MOVE_NAMES, hp = 45, attack = 49, defense = 49, specialAttack = 65, specialDefense = 65, speed = 45 }) => ({
47
+ id,
48
+ name,
49
+ species: {
50
+ name,
51
+ url: `https://pokeapi.co/api/v2/pokemon-species/${speciesId}/`,
52
+ },
53
+ types: [{ slot: 1, type: { name: primaryType } }],
54
+ stats: [
55
+ { base_stat: hp, stat: { name: 'hp' } },
56
+ { base_stat: attack, stat: { name: 'attack' } },
57
+ { base_stat: defense, stat: { name: 'defense' } },
58
+ { base_stat: specialAttack, stat: { name: 'special-attack' } },
59
+ { base_stat: specialDefense, stat: { name: 'special-defense' } },
60
+ { base_stat: speed, stat: { name: 'speed' } },
61
+ ],
62
+ moves: moves.map((moveName) => ({ move: { name: moveName } })),
63
+ sprites: {
64
+ front_default: `https://img.local/${id}-front.png`,
65
+ front_shiny: `https://img.local/${id}-shiny.png`,
66
+ other: {
67
+ 'official-artwork': {
68
+ front_default: `https://img.local/${id}-artwork.png`,
69
+ },
70
+ },
71
+ },
72
+ });
73
+
74
+ const seedCorePokemonData = (cache) => {
75
+ setCache(cache, 'pokemon:1', buildPokemonData({ id: 1, name: 'bulbasaur', primaryType: 'grass', speed: 45 }));
76
+ setCache(cache, 'pokemon:4', buildPokemonData({ id: 4, name: 'charmander', primaryType: 'fire', speed: 65 }));
77
+ setCache(cache, 'pokemon:5', buildPokemonData({ id: 5, name: 'charmeleon', primaryType: 'fire', speciesId: 5, speed: 80 }));
78
+ setCache(cache, 'pokemon:6', buildPokemonData({ id: 6, name: 'charizard', primaryType: 'fire', speciesId: 6, speed: 100 }));
79
+ setCache(cache, 'pokemon:25', buildPokemonData({ id: 25, name: 'pikachu', primaryType: 'electric', speed: 90 }));
80
+ setCache(
81
+ cache,
82
+ 'pokemon:26',
83
+ buildPokemonData({
84
+ id: 26,
85
+ name: 'raichu',
86
+ primaryType: 'electric',
87
+ speciesId: 26,
88
+ speed: 110,
89
+ }),
90
+ );
91
+
92
+ setCache(cache, 'species:1', {
93
+ id: 1,
94
+ capture_rate: 45,
95
+ evolution_chain: { url: 'https://pokeapi.co/api/v2/evolution-chain/1/' },
96
+ });
97
+ setCache(cache, 'species:4', {
98
+ id: 4,
99
+ capture_rate: 45,
100
+ evolution_chain: { url: 'https://pokeapi.co/api/v2/evolution-chain/2/' },
101
+ });
102
+ setCache(cache, 'species:25', {
103
+ id: 25,
104
+ capture_rate: 190,
105
+ evolution_chain: { url: 'https://pokeapi.co/api/v2/evolution-chain/10/' },
106
+ });
107
+ setCache(cache, 'species:26', {
108
+ id: 26,
109
+ capture_rate: 75,
110
+ evolution_chain: { url: 'https://pokeapi.co/api/v2/evolution-chain/10/' },
111
+ });
112
+
113
+ setCache(cache, 'evolution-chain:1', {
114
+ chain: {
115
+ species: { name: 'bulbasaur', url: 'https://pokeapi.co/api/v2/pokemon-species/1/' },
116
+ evolves_to: [
117
+ {
118
+ species: { name: 'ivysaur', url: 'https://pokeapi.co/api/v2/pokemon-species/2/' },
119
+ evolution_details: [{ trigger: { name: 'level-up' }, min_level: 16 }],
120
+ evolves_to: [],
121
+ },
122
+ ],
123
+ },
124
+ });
125
+
126
+ setCache(cache, 'evolution-chain:2', {
127
+ chain: {
128
+ species: { name: 'charmander', url: 'https://pokeapi.co/api/v2/pokemon-species/4/' },
129
+ evolves_to: [
130
+ {
131
+ species: { name: 'charmeleon', url: 'https://pokeapi.co/api/v2/pokemon-species/5/' },
132
+ evolution_details: [{ trigger: { name: 'level-up' }, min_level: 16 }],
133
+ evolves_to: [
134
+ {
135
+ species: { name: 'charizard', url: 'https://pokeapi.co/api/v2/pokemon-species/6/' },
136
+ evolution_details: [{ trigger: { name: 'level-up' }, min_level: 36 }],
137
+ evolves_to: [],
138
+ },
139
+ ],
140
+ },
141
+ ],
142
+ },
143
+ });
144
+
145
+ setCache(cache, 'evolution-chain:10', {
146
+ chain: {
147
+ species: { name: 'pikachu', url: 'https://pokeapi.co/api/v2/pokemon-species/25/' },
148
+ evolves_to: [
149
+ {
150
+ species: { name: 'raichu', url: 'https://pokeapi.co/api/v2/pokemon-species/26/' },
151
+ evolution_details: [{ trigger: { name: 'use-item' }, item: { name: 'thunder-stone' } }],
152
+ evolves_to: [],
153
+ },
154
+ ],
155
+ },
156
+ });
157
+ };
158
+
159
+ const seedCoreTypeAndMoveData = (cache) => {
160
+ setCache(cache, 'type:grass', buildTypeData({ pokemonIds: [1] }));
161
+ setCache(cache, 'type:electric', buildTypeData({ pokemonIds: [25] }));
162
+ setCache(cache, 'type:normal', buildTypeData());
163
+ setCache(cache, 'type:fire', buildTypeData());
164
+ setCache(cache, 'type:water', buildTypeData());
165
+ setCache(cache, 'type:flying', buildTypeData());
166
+ setCache(cache, 'type:dark', buildTypeData());
167
+
168
+ for (const moveName of PREFERRED_MOVE_NAMES) {
169
+ setCache(cache, `move:${moveName}`, buildMoveData(moveName, 'normal', moveName === 'struggle' ? 50 : 40));
170
+ }
171
+
172
+ for (let id = 1; id <= 25; id += 1) {
173
+ setCache(cache, `nature:${id}`, {
174
+ id,
175
+ name: `nature-${id}`,
176
+ increased_stat: null,
177
+ decreased_stat: null,
178
+ });
179
+ }
180
+ };
181
+
182
+ const withRandomSequence = async (values, fn) => {
183
+ const originalRandom = Math.random;
184
+ let index = 0;
185
+
186
+ Math.random = () => {
187
+ if (!values.length) return 0.5;
188
+ const value = values[Math.min(index, values.length - 1)];
189
+ index += 1;
190
+ return value;
191
+ };
192
+
193
+ try {
194
+ return await fn();
195
+ } finally {
196
+ Math.random = originalRandom;
197
+ }
198
+ };
199
+
200
+ const buildMoveSnapshot = ({ name = 'tackle', displayName = 'Tackle', power = 40, accuracy = 100, damageClass = 'physical', type = 'normal', effectMeta = {} } = {}) => ({
201
+ name,
202
+ displayName,
203
+ power,
204
+ accuracy,
205
+ damageClass,
206
+ type,
207
+ typeDamage: { doubleTo: [], halfTo: [], noTo: [] },
208
+ effectMeta: {
209
+ target: effectMeta.target || 'selected-pokemon',
210
+ statChanges: Array.isArray(effectMeta.statChanges) ? effectMeta.statChanges : [],
211
+ statChance: Number.isFinite(Number(effectMeta.statChance)) ? Number(effectMeta.statChance) : 100,
212
+ ailment: effectMeta.ailment || null,
213
+ ailmentChance: Number.isFinite(Number(effectMeta.ailmentChance)) ? Number(effectMeta.ailmentChance) : 0,
214
+ healing: Number.isFinite(Number(effectMeta.healing)) ? Number(effectMeta.healing) : 0,
215
+ drain: Number.isFinite(Number(effectMeta.drain)) ? Number(effectMeta.drain) : 0,
216
+ },
217
+ });
218
+
219
+ test('fluxo crítico: explorar -> batalha -> capturar deve ser executável com snapshot válido', { concurrency: false }, async () => {
220
+ const cache = ensurePokeApiCache();
221
+ cache.clear();
222
+ seedCoreTypeAndMoveData(cache);
223
+ seedCorePokemonData(cache);
224
+
225
+ const encounter = await withRandomSequence([0.4, 0.8, 0.0, 0.0], async () =>
226
+ createWildEncounter({
227
+ playerLevel: 8,
228
+ preferredTypes: ['grass'],
229
+ }),
230
+ );
231
+
232
+ assert.equal(encounter.enemySnapshot.pokeId, 1);
233
+ assert.equal(encounter.enemySnapshot.isShiny, false);
234
+
235
+ const battleSnapshot = {
236
+ my: {
237
+ displayName: 'Pikachu',
238
+ level: 12,
239
+ currentHp: 55,
240
+ maxHp: 55,
241
+ types: ['electric'],
242
+ stats: {
243
+ attack: 80,
244
+ defense: 60,
245
+ specialAttack: 80,
246
+ specialDefense: 60,
247
+ speed: 110,
248
+ },
249
+ moves: [
250
+ {
251
+ displayName: 'Tackle',
252
+ name: 'tackle',
253
+ power: 40,
254
+ accuracy: 100,
255
+ damageClass: 'physical',
256
+ type: 'normal',
257
+ typeDamage: { doubleTo: [], halfTo: [], noTo: [] },
258
+ },
259
+ {
260
+ displayName: 'Tackle',
261
+ name: 'tackle',
262
+ power: 40,
263
+ accuracy: 100,
264
+ damageClass: 'physical',
265
+ type: 'normal',
266
+ typeDamage: { doubleTo: [], halfTo: [], noTo: [] },
267
+ },
268
+ {
269
+ displayName: 'Tackle',
270
+ name: 'tackle',
271
+ power: 40,
272
+ accuracy: 100,
273
+ damageClass: 'physical',
274
+ type: 'normal',
275
+ typeDamage: { doubleTo: [], halfTo: [], noTo: [] },
276
+ },
277
+ {
278
+ displayName: 'Tackle',
279
+ name: 'tackle',
280
+ power: 40,
281
+ accuracy: 100,
282
+ damageClass: 'physical',
283
+ type: 'normal',
284
+ typeDamage: { doubleTo: [], halfTo: [], noTo: [] },
285
+ },
286
+ ],
287
+ },
288
+ enemy: encounter.enemySnapshot,
289
+ };
290
+
291
+ const turnResult = await withRandomSequence([0.0, 0.0, 0.0, 0.0], async () =>
292
+ resolveBattleTurn({
293
+ battleSnapshot,
294
+ playerMoveSlot: 1,
295
+ }),
296
+ );
297
+
298
+ assert.equal(turnResult.validTurn, true);
299
+ assert.ok(turnResult.logs.length > 0);
300
+
301
+ turnResult.snapshot.enemy.currentHp = 1;
302
+ turnResult.snapshot.my.currentHp = Math.max(1, turnResult.snapshot.my.currentHp);
303
+
304
+ const captureResult = await withRandomSequence([0.0], async () =>
305
+ resolveCaptureAttempt({
306
+ battleSnapshot: turnResult.snapshot,
307
+ }),
308
+ );
309
+
310
+ assert.equal(captureResult.validAction, true);
311
+ assert.equal(captureResult.success, true);
312
+ assert.equal(captureResult.winner, 'player');
313
+ });
314
+
315
+ test('deve marcar encontro como shiny e usar sprite shiny quando o roll atender a chance', { concurrency: false }, async () => {
316
+ const cache = ensurePokeApiCache();
317
+ cache.clear();
318
+ seedCoreTypeAndMoveData(cache);
319
+ seedCorePokemonData(cache);
320
+
321
+ const encounter = await withRandomSequence([0.4, 0.0, 0.0, 0.0], async () =>
322
+ createWildEncounter({
323
+ playerLevel: 10,
324
+ preferredTypes: ['grass'],
325
+ }),
326
+ );
327
+
328
+ assert.equal(encounter.isShiny, true);
329
+ assert.equal(encounter.enemySnapshot.isShiny, true);
330
+ assert.equal(encounter.enemySnapshot.imageUrl, 'https://img.local/1-shiny.png');
331
+ });
332
+
333
+ test('bioma deve priorizar spawn por tipo preferencial', { concurrency: false }, async () => {
334
+ const cache = ensurePokeApiCache();
335
+ cache.clear();
336
+ seedCoreTypeAndMoveData(cache);
337
+ seedCorePokemonData(cache);
338
+
339
+ const encounter = await withRandomSequence([0.6, 0.9, 0.0, 0.0], async () =>
340
+ createWildEncounter({
341
+ playerLevel: 10,
342
+ preferredTypes: ['electric'],
343
+ }),
344
+ );
345
+
346
+ assert.equal(encounter.enemySnapshot.pokeId, 25);
347
+ assert.ok(encounter.enemySnapshot.types.includes('electric'));
348
+ });
349
+
350
+ test('moveset inicial deve garantir STAB e limitar normal quando houver alternativas', { concurrency: false }, async () => {
351
+ const cache = ensurePokeApiCache();
352
+ cache.clear();
353
+ seedCoreTypeAndMoveData(cache);
354
+
355
+ setCache(
356
+ cache,
357
+ 'pokemon:1',
358
+ buildPokemonData({
359
+ id: 1,
360
+ name: 'bulbasaur',
361
+ primaryType: 'grass',
362
+ moves: ['tackle', 'scratch', 'vine-whip', 'bite', 'growl', 'struggle'],
363
+ }),
364
+ );
365
+ setCache(cache, 'species:1', {
366
+ id: 1,
367
+ capture_rate: 45,
368
+ evolution_chain: { url: 'https://pokeapi.co/api/v2/evolution-chain/1/' },
369
+ });
370
+
371
+ setCache(cache, 'type:grass', buildTypeData({ pokemonIds: [1] }));
372
+ setCache(cache, 'type:normal', buildTypeData({ noDamageTo: ['ghost'] }));
373
+ setCache(cache, 'type:dark', buildTypeData());
374
+
375
+ setCache(cache, 'move:tackle', buildMoveData('tackle', 'normal', 40));
376
+ setCache(cache, 'move:scratch', buildMoveData('scratch', 'normal', 40));
377
+ setCache(cache, 'move:vine-whip', buildMoveData('vine-whip', 'grass', 45));
378
+ setCache(cache, 'move:bite', buildMoveData('bite', 'dark', 60));
379
+ setCache(cache, 'move:growl', buildMoveData('growl', 'normal', 0));
380
+ setCache(cache, 'move:struggle', buildMoveData('struggle', 'normal', 50));
381
+
382
+ const encounter = await withRandomSequence([0.8, 0.4, 0.2, 0.1], async () =>
383
+ createWildEncounter({
384
+ playerLevel: 10,
385
+ preferredTypes: ['grass'],
386
+ }),
387
+ );
388
+
389
+ const offensiveMoves = encounter.enemySnapshot.moves.filter((move) => Number(move?.power) > 0 && move?.damageClass !== 'status');
390
+ const hasStab = offensiveMoves.some((move) => String(move?.type || '').toLowerCase() === 'grass');
391
+ const normalCount = offensiveMoves.filter((move) => String(move?.type || '').toLowerCase() === 'normal').length;
392
+
393
+ assert.equal(hasStab, true);
394
+ assert.ok(normalCount <= 1);
395
+ });
396
+
397
+ test('moveset inicial deve evitar softlock por imunidade com fallback neutro', { concurrency: false }, async () => {
398
+ const cache = ensurePokeApiCache();
399
+ cache.clear();
400
+ seedCoreTypeAndMoveData(cache);
401
+
402
+ setCache(
403
+ cache,
404
+ 'pokemon:66',
405
+ buildPokemonData({
406
+ id: 66,
407
+ name: 'machop',
408
+ primaryType: 'fighting',
409
+ moves: ['karate-chop', 'tackle', 'leer', 'struggle'],
410
+ }),
411
+ );
412
+ setCache(cache, 'species:66', {
413
+ id: 66,
414
+ capture_rate: 180,
415
+ evolution_chain: { url: 'https://pokeapi.co/api/v2/evolution-chain/23/' },
416
+ });
417
+
418
+ setCache(cache, 'type:fighting', buildTypeData({ pokemonIds: [66], noDamageTo: ['ghost'] }));
419
+ setCache(cache, 'type:normal', buildTypeData({ noDamageTo: ['ghost'] }));
420
+
421
+ setCache(cache, 'move:karate-chop', buildMoveData('karate-chop', 'fighting', 50));
422
+ setCache(cache, 'move:tackle', buildMoveData('tackle', 'normal', 40));
423
+ setCache(cache, 'move:leer', buildMoveData('leer', 'normal', 0));
424
+ setCache(cache, 'move:struggle', buildMoveData('struggle', 'normal', 50));
425
+
426
+ const encounter = await withRandomSequence([0.8, 0.4, 0.2, 0.1], async () =>
427
+ createWildEncounter({
428
+ playerLevel: 14,
429
+ preferredTypes: ['fighting'],
430
+ }),
431
+ );
432
+
433
+ const offensiveMoves = encounter.enemySnapshot.moves.filter((move) => Number(move?.power) > 0 && move?.damageClass !== 'status');
434
+ const canHitGhost = offensiveMoves.some((move) => !(move?.typeDamage?.noTo || []).includes('ghost'));
435
+ const hasNeutralFallback = encounter.enemySnapshot.moves.some((move) => String(move?.name || '').startsWith('neutral-strike'));
436
+
437
+ assert.equal(canHitGhost, true);
438
+ assert.equal(hasNeutralFallback, true);
439
+ });
440
+
441
+ test('evolução automática por nível deve seguir evolution-chain da PokéAPI', { concurrency: false }, async () => {
442
+ const cache = ensurePokeApiCache();
443
+ cache.clear();
444
+ seedCoreTypeAndMoveData(cache);
445
+ seedCorePokemonData(cache);
446
+
447
+ const atLevel16 = await resolveEvolutionByLevel({
448
+ pokeId: 4,
449
+ level: 16,
450
+ });
451
+ assert.equal(atLevel16?.from?.pokeId, 4);
452
+ assert.equal(atLevel16?.to?.pokeId, 5);
453
+ assert.equal(atLevel16?.to?.name, 'Charmeleon');
454
+
455
+ const atLevel40 = await resolveEvolutionByLevel({
456
+ pokeId: 4,
457
+ level: 40,
458
+ });
459
+ assert.equal(atLevel40?.to?.pokeId, 6);
460
+ assert.equal(atLevel40?.to?.name, 'Charizard');
461
+ });
462
+
463
+ test('evolução por nível deve retornar nulo quando ainda não atingiu requisito', { concurrency: false }, async () => {
464
+ const cache = ensurePokeApiCache();
465
+ cache.clear();
466
+ seedCoreTypeAndMoveData(cache);
467
+ seedCorePokemonData(cache);
468
+
469
+ const noEvolution = await resolveEvolutionByLevel({
470
+ pokeId: 1,
471
+ level: 15,
472
+ });
473
+ assert.equal(noEvolution, null);
474
+ });
475
+
476
+ test('evolução por item deve falhar para item inválido', { concurrency: false }, async () => {
477
+ const cache = ensurePokeApiCache();
478
+ cache.clear();
479
+ seedCoreTypeAndMoveData(cache);
480
+ seedCorePokemonData(cache);
481
+
482
+ const invalidItem = await resolveEvolutionByItem({
483
+ pokeId: 25,
484
+ itemKey: 'water-stone',
485
+ });
486
+ assert.equal(invalidItem, null);
487
+ });
488
+
489
+ test('evolução por item válida deve resolver próximo estágio', { concurrency: false }, async () => {
490
+ const cache = ensurePokeApiCache();
491
+ cache.clear();
492
+ seedCoreTypeAndMoveData(cache);
493
+ seedCorePokemonData(cache);
494
+
495
+ const validItem = await resolveEvolutionByItem({
496
+ pokeId: 25,
497
+ itemKey: 'thunder-stone',
498
+ });
499
+ assert.equal(validItem?.from?.pokeId, 25);
500
+ assert.equal(validItem?.to?.pokeId, 26);
501
+ assert.equal(validItem?.to?.name, 'Raichu');
502
+ });
503
+
504
+ test('ataque único deve aplicar dano e reduzir HP do alvo', () => {
505
+ const attacker = {
506
+ displayName: 'Pikachu',
507
+ level: 15,
508
+ types: ['electric'],
509
+ stats: {
510
+ attack: 70,
511
+ defense: 55,
512
+ specialAttack: 65,
513
+ specialDefense: 55,
514
+ speed: 90,
515
+ },
516
+ moves: [
517
+ {
518
+ displayName: 'Tackle',
519
+ name: 'tackle',
520
+ power: 40,
521
+ accuracy: 100,
522
+ damageClass: 'physical',
523
+ type: 'normal',
524
+ typeDamage: { doubleTo: [], halfTo: [], noTo: [] },
525
+ },
526
+ ],
527
+ };
528
+ const defender = {
529
+ displayName: 'Bulbasaur',
530
+ level: 12,
531
+ currentHp: 80,
532
+ maxHp: 80,
533
+ types: ['grass'],
534
+ stats: {
535
+ attack: 55,
536
+ defense: 60,
537
+ specialAttack: 55,
538
+ specialDefense: 60,
539
+ speed: 50,
540
+ },
541
+ moves: [],
542
+ };
543
+
544
+ const result = resolveSingleAttack({
545
+ attackerSnapshot: attacker,
546
+ defenderSnapshot: defender,
547
+ moveSlot: 1,
548
+ attackerLabel: 'Pikachu',
549
+ defenderLabel: 'Bulbasaur',
550
+ });
551
+
552
+ assert.equal(result.validMove, true);
553
+ assert.ok(result.damage >= 1);
554
+ assert.ok(result.defender.currentHp < 80);
555
+ });
556
+
557
+ test('paralisia deve poder impedir ação no turno', async () => {
558
+ const caster = {
559
+ displayName: 'Pikachu',
560
+ level: 20,
561
+ currentHp: 95,
562
+ maxHp: 95,
563
+ types: ['electric'],
564
+ stats: {
565
+ attack: 70,
566
+ defense: 55,
567
+ specialAttack: 80,
568
+ specialDefense: 60,
569
+ speed: 95,
570
+ },
571
+ moves: [
572
+ buildMoveSnapshot({
573
+ name: 'thunder-wave',
574
+ displayName: 'Thunder Wave',
575
+ power: 0,
576
+ damageClass: 'status',
577
+ type: 'electric',
578
+ effectMeta: {
579
+ target: 'selected-pokemon',
580
+ ailment: 'paralysis',
581
+ ailmentChance: 100,
582
+ },
583
+ }),
584
+ ],
585
+ };
586
+
587
+ const target = {
588
+ displayName: 'Eevee',
589
+ level: 18,
590
+ currentHp: 100,
591
+ maxHp: 100,
592
+ types: ['normal'],
593
+ stats: {
594
+ attack: 65,
595
+ defense: 60,
596
+ specialAttack: 45,
597
+ specialDefense: 65,
598
+ speed: 55,
599
+ },
600
+ moves: [buildMoveSnapshot()],
601
+ };
602
+
603
+ const setup = await withRandomSequence([0.0], async () =>
604
+ resolveSingleAttack({
605
+ attackerSnapshot: caster,
606
+ defenderSnapshot: target,
607
+ moveSlot: 1,
608
+ attackerLabel: 'Pikachu',
609
+ defenderLabel: 'Eevee',
610
+ }),
611
+ );
612
+
613
+ assert.equal(setup.defender.nonVolatileStatus, 'paralysis');
614
+ assert.ok(setup.logs.some((line) => String(line).includes('paralisado')));
615
+
616
+ const blocked = await withRandomSequence([0.0], async () =>
617
+ resolveSingleAttack({
618
+ attackerSnapshot: setup.defender,
619
+ defenderSnapshot: setup.attacker,
620
+ moveSlot: 1,
621
+ attackerLabel: 'Eevee',
622
+ defenderLabel: 'Pikachu',
623
+ }),
624
+ );
625
+
626
+ assert.equal(blocked.validMove, true);
627
+ assert.equal(blocked.damage, 0);
628
+ assert.ok(blocked.logs.some((line) => String(line).includes('não conseguiu agir')));
629
+ });
630
+
631
+ test('debuff de ataque deve reduzir dano físico no turno seguinte', async () => {
632
+ const attackerBase = {
633
+ displayName: 'GrowlMon',
634
+ level: 22,
635
+ currentHp: 120,
636
+ maxHp: 120,
637
+ types: ['normal'],
638
+ stats: {
639
+ attack: 70,
640
+ defense: 70,
641
+ specialAttack: 60,
642
+ specialDefense: 65,
643
+ speed: 75,
644
+ },
645
+ moves: [
646
+ buildMoveSnapshot({
647
+ name: 'growl',
648
+ displayName: 'Growl',
649
+ power: 0,
650
+ damageClass: 'status',
651
+ effectMeta: {
652
+ target: 'selected-pokemon',
653
+ statChanges: [{ stat: 'attack', change: -1 }],
654
+ statChance: 100,
655
+ },
656
+ }),
657
+ ],
658
+ };
659
+
660
+ const defenderBase = {
661
+ displayName: 'BiteMon',
662
+ level: 22,
663
+ currentHp: 120,
664
+ maxHp: 120,
665
+ types: ['normal'],
666
+ stats: {
667
+ attack: 85,
668
+ defense: 68,
669
+ specialAttack: 50,
670
+ specialDefense: 60,
671
+ speed: 72,
672
+ },
673
+ moves: [buildMoveSnapshot()],
674
+ };
675
+
676
+ const baseline = await withRandomSequence([0.0, 0.0], async () =>
677
+ resolveSingleAttack({
678
+ attackerSnapshot: defenderBase,
679
+ defenderSnapshot: attackerBase,
680
+ moveSlot: 1,
681
+ attackerLabel: 'BiteMon',
682
+ defenderLabel: 'GrowlMon',
683
+ }),
684
+ );
685
+
686
+ const debuffStep = await withRandomSequence([0.0], async () =>
687
+ resolveSingleAttack({
688
+ attackerSnapshot: attackerBase,
689
+ defenderSnapshot: defenderBase,
690
+ moveSlot: 1,
691
+ attackerLabel: 'GrowlMon',
692
+ defenderLabel: 'BiteMon',
693
+ }),
694
+ );
695
+
696
+ assert.ok(debuffStep.logs.some((line) => String(line).includes('reduziu Ataque')));
697
+
698
+ const afterDebuff = await withRandomSequence([0.0, 0.0], async () =>
699
+ resolveSingleAttack({
700
+ attackerSnapshot: debuffStep.defender,
701
+ defenderSnapshot: debuffStep.attacker,
702
+ moveSlot: 1,
703
+ attackerLabel: 'BiteMon',
704
+ defenderLabel: 'GrowlMon',
705
+ }),
706
+ );
707
+
708
+ assert.ok(afterDebuff.damage < baseline.damage);
709
+ });
710
+
711
+ test('queimadura deve causar dano residual ao final do turno', async () => {
712
+ const caster = {
713
+ displayName: 'Flareon',
714
+ level: 24,
715
+ currentHp: 130,
716
+ maxHp: 130,
717
+ types: ['fire'],
718
+ stats: {
719
+ attack: 95,
720
+ defense: 70,
721
+ specialAttack: 85,
722
+ specialDefense: 80,
723
+ speed: 70,
724
+ },
725
+ moves: [
726
+ buildMoveSnapshot({
727
+ name: 'will-o-wisp',
728
+ displayName: 'Will-O-Wisp',
729
+ power: 0,
730
+ damageClass: 'status',
731
+ type: 'fire',
732
+ effectMeta: {
733
+ target: 'selected-pokemon',
734
+ ailment: 'burn',
735
+ ailmentChance: 100,
736
+ },
737
+ }),
738
+ ],
739
+ };
740
+
741
+ const target = {
742
+ displayName: 'Snorlax',
743
+ level: 24,
744
+ currentHp: 160,
745
+ maxHp: 160,
746
+ types: ['normal'],
747
+ stats: {
748
+ attack: 90,
749
+ defense: 75,
750
+ specialAttack: 65,
751
+ specialDefense: 85,
752
+ speed: 30,
753
+ },
754
+ moves: [buildMoveSnapshot()],
755
+ };
756
+
757
+ const result = await withRandomSequence([0.0], async () =>
758
+ resolveSingleAttack({
759
+ attackerSnapshot: caster,
760
+ defenderSnapshot: target,
761
+ moveSlot: 1,
762
+ attackerLabel: 'Flareon',
763
+ defenderLabel: 'Snorlax',
764
+ }),
765
+ );
766
+
767
+ assert.equal(result.defender.nonVolatileStatus, 'burn');
768
+ assert.ok(result.defender.currentHp < target.maxHp);
769
+ assert.ok(result.logs.some((line) => String(line).includes('dano por queimadura')));
770
+ });