@kaikybrofc/omnizap-system 2.1.8

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 (166) hide show
  1. package/.env.example +534 -0
  2. package/LICENSE +21 -0
  3. package/README.md +431 -0
  4. package/RELEASE-v2.1.2.md +83 -0
  5. package/app/config/adminIdentity.js +87 -0
  6. package/app/config/baileysConfig.js +693 -0
  7. package/app/config/groupUtils.js +388 -0
  8. package/app/connection/socketController.js +992 -0
  9. package/app/controllers/messageController.js +354 -0
  10. package/app/modules/adminModule/groupCommandHandlers.js +1294 -0
  11. package/app/modules/adminModule/groupEventHandlers.js +355 -0
  12. package/app/modules/aiModule/catCommand.js +1006 -0
  13. package/app/modules/broadcastModule/noticeCommand.js +416 -0
  14. package/app/modules/gameModule/diceCommand.js +67 -0
  15. package/app/modules/menuModule/common.js +311 -0
  16. package/app/modules/menuModule/menus.js +59 -0
  17. package/app/modules/playModule/playCommand.js +1615 -0
  18. package/app/modules/quoteModule/quoteCommand.js +851 -0
  19. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +786 -0
  20. package/app/modules/rpgPokemonModule/rpgBattleService.js +2082 -0
  21. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +760 -0
  22. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  23. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +172 -0
  24. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  25. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  26. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  27. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  28. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1859 -0
  29. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6738 -0
  30. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  31. package/app/modules/statsModule/globalRankingCommand.js +65 -0
  32. package/app/modules/statsModule/noMessageCommand.js +288 -0
  33. package/app/modules/statsModule/rankingCommand.js +60 -0
  34. package/app/modules/statsModule/rankingCommon.js +889 -0
  35. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  36. package/app/modules/stickerModule/convertToWebp.js +390 -0
  37. package/app/modules/stickerModule/stickerCommand.js +454 -0
  38. package/app/modules/stickerModule/stickerConvertCommand.js +156 -0
  39. package/app/modules/stickerModule/stickerTextCommand.js +657 -0
  40. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  41. package/app/modules/stickerPackModule/autoPackCollectorService.js +284 -0
  42. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +466 -0
  43. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +88 -0
  44. package/app/modules/stickerPackModule/semanticThemeClusterService.js +571 -0
  45. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +449 -0
  46. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  47. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +180 -0
  48. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +4078 -0
  49. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +598 -0
  50. package/app/modules/stickerPackModule/stickerClassificationService.js +588 -0
  51. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +102 -0
  52. package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +7506 -0
  53. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1095 -0
  54. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +108 -0
  55. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  56. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +110 -0
  57. package/app/modules/stickerPackModule/stickerPackItemRepository.js +440 -0
  58. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +337 -0
  59. package/app/modules/stickerPackModule/stickerPackMessageService.js +296 -0
  60. package/app/modules/stickerPackModule/stickerPackRepository.js +442 -0
  61. package/app/modules/stickerPackModule/stickerPackService.js +788 -0
  62. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +51 -0
  63. package/app/modules/stickerPackModule/stickerPackUtils.js +97 -0
  64. package/app/modules/stickerPackModule/stickerStorageService.js +507 -0
  65. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +233 -0
  66. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +205 -0
  67. package/app/modules/systemMetricsModule/pingCommand.js +421 -0
  68. package/app/modules/tiktokModule/tiktokCommand.js +798 -0
  69. package/app/modules/userModule/userCommand.js +1217 -0
  70. package/app/modules/waifuPicsModule/waifuPicsCommand.js +177 -0
  71. package/app/observability/metrics.js +734 -0
  72. package/app/services/captchaService.js +492 -0
  73. package/app/services/dbWriteQueue.js +572 -0
  74. package/app/services/groupMetadataService.js +279 -0
  75. package/app/services/lidMapService.js +663 -0
  76. package/app/services/messagePersistenceService.js +56 -0
  77. package/app/services/newsBroadcastService.js +351 -0
  78. package/app/services/pokeApiService.js +398 -0
  79. package/app/services/queueUtils.js +57 -0
  80. package/app/services/socketState.js +7 -0
  81. package/app/store/aiPromptStore.js +38 -0
  82. package/app/store/groupConfigStore.js +58 -0
  83. package/app/store/premiumUserStore.js +36 -0
  84. package/app/utils/antiLink/antiLinkModule.js +804 -0
  85. package/app/utils/http/getImageBufferModule.js +18 -0
  86. package/app/utils/json/jsonSanitizer.js +113 -0
  87. package/app/utils/json/jsonSanitizer.test.js +40 -0
  88. package/app/utils/logger/loggerModule.js +262 -0
  89. package/app/utils/systemMetrics/systemMetricsModule.js +91 -0
  90. package/database/index.js +2052 -0
  91. package/database/init.js +516 -0
  92. package/database/migrations/20260203_0001_sticker_packs.sql +54 -0
  93. package/database/migrations/20260210_0003_rpg_pokemon.sql +58 -0
  94. package/database/migrations/20260210_0004_rpg_shiny_biome.sql +9 -0
  95. package/database/migrations/20260210_0005_rpg_missions.sql +14 -0
  96. package/database/migrations/20260210_0006_rpg_world_pokedex_traits.sql +27 -0
  97. package/database/migrations/20260210_0007_rpg_raid_pvp.sql +56 -0
  98. package/database/migrations/20260210_0008_rpg_social_system.sql +195 -0
  99. package/database/migrations/20260211_0009_rpg_social_xp.sql +36 -0
  100. package/database/migrations/20260222_0010_remove_message_xp.sql +2 -0
  101. package/database/migrations/20260226_0011_sticker_asset_classification.sql +17 -0
  102. package/database/migrations/20260226_0012_sticker_pack_engagement.sql +16 -0
  103. package/database/migrations/20260226_0013_sticker_marketplace_intelligence.sql +19 -0
  104. package/database/migrations/20260226_0014_sticker_pack_publish_flow.sql +30 -0
  105. package/database/migrations/20260226_0014_sticker_worker_queues.sql +42 -0
  106. package/database/migrations/20260226_0015_sticker_auto_pack_curation_integrity.sql +18 -0
  107. package/database/migrations/20260226_0016_sticker_web_google_auth_persistence.sql +34 -0
  108. package/database/migrations/20260226_0017_sticker_web_admin_ban.sql +22 -0
  109. package/database/migrations/20260226_0018_sticker_web_admin_moderator.sql +18 -0
  110. package/database/migrations/20260227_0019_sticker_classification_v2_signals.sql +12 -0
  111. package/database/migrations/20260227_0020_semantic_theme_clusters.sql +35 -0
  112. package/docker-compose.yml +103 -0
  113. package/ecosystem.prod.config.cjs +35 -0
  114. package/eslint.config.js +61 -0
  115. package/index.js +437 -0
  116. package/ml/clip_classifier/Dockerfile +16 -0
  117. package/ml/clip_classifier/README.md +120 -0
  118. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  119. package/ml/clip_classifier/classifier.py +654 -0
  120. package/ml/clip_classifier/embedding_store.py +481 -0
  121. package/ml/clip_classifier/env_loader.py +15 -0
  122. package/ml/clip_classifier/llm_label_expander.py +144 -0
  123. package/ml/clip_classifier/main.py +213 -0
  124. package/ml/clip_classifier/requirements.txt +10 -0
  125. package/ml/clip_classifier/similarity_engine.py +74 -0
  126. package/observability/alert-rules.yml +60 -0
  127. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  128. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  129. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  130. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  131. package/observability/loki-config.yml +38 -0
  132. package/observability/mysql-exporter.cnf +5 -0
  133. package/observability/mysql-setup.sql +46 -0
  134. package/observability/prometheus.yml +32 -0
  135. package/observability/promtail-config.yml +84 -0
  136. package/package.json +109 -0
  137. package/public/api-docs/index.html +144 -0
  138. package/public/css/github-project-panel.css +297 -0
  139. package/public/css/stickers-admin.css +1272 -0
  140. package/public/css/styles.css +671 -0
  141. package/public/index.html +1311 -0
  142. package/public/js/apps/apiDocsApp.js +310 -0
  143. package/public/js/apps/createPackApp.js +2069 -0
  144. package/public/js/apps/homeApp.js +396 -0
  145. package/public/js/apps/stickersAdminApp.js +1744 -0
  146. package/public/js/apps/stickersApp.js +4830 -0
  147. package/public/js/catalog.js +1019 -0
  148. package/public/js/github-panel/components/CommitList.js +34 -0
  149. package/public/js/github-panel/components/ErrorState.js +16 -0
  150. package/public/js/github-panel/components/GithubProjectPanel.js +106 -0
  151. package/public/js/github-panel/components/ReleaseList.js +38 -0
  152. package/public/js/github-panel/components/SkeletonPanel.js +22 -0
  153. package/public/js/github-panel/components/StatCard.js +15 -0
  154. package/public/js/github-panel/index.js +15 -0
  155. package/public/js/github-panel/useGithubRepoData.js +154 -0
  156. package/public/js/github-panel/vendor/react.js +11 -0
  157. package/public/js/runtime/react-runtime.js +19 -0
  158. package/public/licenca/index.html +106 -0
  159. package/public/stickers/admin/index.html +23 -0
  160. package/public/stickers/create/index.html +47 -0
  161. package/public/stickers/index.html +48 -0
  162. package/public/termos-de-uso/index.html +125 -0
  163. package/scripts/cache-bust.mjs +107 -0
  164. package/scripts/deploy.sh +458 -0
  165. package/scripts/github-deploy-notify.mjs +174 -0
  166. package/scripts/release.sh +129 -0
@@ -0,0 +1,22 @@
1
+ const toInt = (value, fallback = 0) => {
2
+ const parsed = Number(value);
3
+ return Number.isFinite(parsed) ? Math.trunc(parsed) : fallback;
4
+ };
5
+
6
+ export const registerEvolutionPokedexEntry = async ({ ownerJid, evolutionOutcome, connection = null, registerEntry }) => {
7
+ if (typeof registerEntry !== 'function') {
8
+ throw new Error('registerEntry é obrigatório para registrar evolução na Pokédex.');
9
+ }
10
+
11
+ const evolvedPokeId = toInt(evolutionOutcome?.updatePayload?.pokeId, 0);
12
+ if (!ownerJid || evolvedPokeId <= 0) return false;
13
+
14
+ await registerEntry(
15
+ {
16
+ ownerJid,
17
+ pokeId: evolvedPokeId,
18
+ },
19
+ connection,
20
+ );
21
+ return true;
22
+ };
@@ -0,0 +1,172 @@
1
+ import { sendAndStore } from '../../services/messagePersistenceService.js';
2
+ import { resolveUserIdCached } from '../../services/lidMapService.js';
3
+ import logger from '../../utils/logger/loggerModule.js';
4
+ import { buildRpgHelpText, buildUsageText } from './rpgPokemonMessages.js';
5
+ import { executeRpgPokemonAction } from './rpgPokemonService.js';
6
+
7
+ const ALLOWED_ACTIONS = new Set(['help', 'ajuda', 'start', 'perfil', 'explorar', 'atacar', 'capturar', 'fugir', 'time', 'escolher', 'loja', 'comprar', 'usar', 'bolsa', 'pokedex', 'evolucao', 'evolução', 'missoes', 'missões', 'viajar', 'tm', 'berry', 'raid', 'desafiar', 'pvp', 'ginasio', 'ginásio', 'trade', 'coop', 'evento', 'social', 'karma', 'engajamento']);
8
+ const HELP_ACTIONS = new Set(['help', 'ajuda']);
9
+
10
+ const getContextInfo = (messageInfo) => {
11
+ const root = messageInfo?.message;
12
+ if (!root || typeof root !== 'object') return null;
13
+
14
+ for (const value of Object.values(root)) {
15
+ if (value?.contextInfo && typeof value.contextInfo === 'object') {
16
+ return value.contextInfo;
17
+ }
18
+ if (value?.message && typeof value.message === 'object') {
19
+ for (const nested of Object.values(value.message)) {
20
+ if (nested?.contextInfo && typeof nested.contextInfo === 'object') {
21
+ return nested.contextInfo;
22
+ }
23
+ }
24
+ }
25
+ }
26
+
27
+ return null;
28
+ };
29
+
30
+ const extractMentionedJids = (messageInfo) => {
31
+ const contextInfo = getContextInfo(messageInfo);
32
+ if (!Array.isArray(contextInfo?.mentionedJid)) return [];
33
+ return Array.from(new Set(contextInfo.mentionedJid.filter((jid) => typeof jid === 'string' && jid.trim())));
34
+ };
35
+
36
+ const resolveOwnerJid = ({ senderJid, senderIdentity }) => {
37
+ if (senderIdentity && typeof senderIdentity === 'object') {
38
+ const resolved = resolveUserIdCached({
39
+ jid: senderIdentity?.jid || senderJid || null,
40
+ lid: senderIdentity?.participant || senderIdentity?.jid || senderJid || null,
41
+ participantAlt: senderIdentity?.participantAlt || null,
42
+ });
43
+
44
+ if (resolved) return resolved;
45
+ }
46
+
47
+ if (senderJid && typeof senderJid === 'string') {
48
+ return (
49
+ resolveUserIdCached({
50
+ jid: senderJid,
51
+ lid: senderJid,
52
+ participantAlt: null,
53
+ }) || senderJid
54
+ );
55
+ }
56
+
57
+ return null;
58
+ };
59
+
60
+ export const handleRpgPokemonCommand = async ({ sock, remoteJid, messageInfo, expirationMessage, senderJid, senderIdentity = null, args = [], commandPrefix = '/' }) => {
61
+ const action = String(args?.[0] || '')
62
+ .trim()
63
+ .toLowerCase();
64
+
65
+ if (!action) {
66
+ await sendAndStore(sock, remoteJid, { text: buildRpgHelpText(commandPrefix) }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
67
+ return;
68
+ }
69
+
70
+ if (HELP_ACTIONS.has(action)) {
71
+ await sendAndStore(sock, remoteJid, { text: buildRpgHelpText(commandPrefix) }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
72
+ return;
73
+ }
74
+
75
+ if (!ALLOWED_ACTIONS.has(action)) {
76
+ await sendAndStore(
77
+ sock,
78
+ remoteJid,
79
+ {
80
+ text: `${buildUsageText(commandPrefix)}\n\nUse um subcomando válido depois de ${commandPrefix}rpg.\nGuia completo: ${commandPrefix}rpg help`,
81
+ },
82
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
83
+ );
84
+ return;
85
+ }
86
+
87
+ const ownerJid = resolveOwnerJid({ senderJid, senderIdentity });
88
+ if (!ownerJid) {
89
+ await sendAndStore(sock, remoteJid, { text: '❌ Não foi possível identificar o jogador para o RPG.' }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
90
+ return;
91
+ }
92
+
93
+ try {
94
+ const result = await executeRpgPokemonAction({
95
+ ownerJid,
96
+ chatJid: remoteJid,
97
+ action,
98
+ actionArgs: args.slice(1),
99
+ mentionedJids: extractMentionedJids(messageInfo),
100
+ commandPrefix,
101
+ });
102
+
103
+ const responseText = result?.text || '❌ Não foi possível processar o comando RPG agora.';
104
+ const mentions = Array.isArray(result?.mentions) ? result.mentions.filter(Boolean) : [];
105
+ const imageBuffer = Buffer.isBuffer(result?.imageBuffer) ? result.imageBuffer : null;
106
+ const imageUrl = typeof result?.imageUrl === 'string' && result.imageUrl.trim() ? result.imageUrl.trim() : null;
107
+ const resultCaption = typeof result?.caption === 'string' && result.caption.trim() ? result.caption.trim() : null;
108
+ const caption = result?.preferResultCaption && resultCaption ? resultCaption : responseText;
109
+ const sendTextAfterImage = Boolean(result?.sendTextAfterImage);
110
+
111
+ if (imageBuffer) {
112
+ try {
113
+ await sendAndStore(
114
+ sock,
115
+ remoteJid,
116
+ {
117
+ image: imageBuffer,
118
+ caption,
119
+ ...(mentions.length ? { mentions } : {}),
120
+ },
121
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
122
+ );
123
+ if (sendTextAfterImage && responseText && responseText !== caption) {
124
+ await sendAndStore(sock, remoteJid, { text: responseText, ...(mentions.length ? { mentions } : {}) }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
125
+ }
126
+ return;
127
+ } catch (error) {
128
+ logger.warn('Falha ao enviar frame canvas do RPG Pokemon. Fallback para imagem URL/texto.', {
129
+ ownerJid,
130
+ action,
131
+ error: error.message,
132
+ });
133
+ }
134
+ }
135
+
136
+ if (imageUrl) {
137
+ try {
138
+ await sendAndStore(
139
+ sock,
140
+ remoteJid,
141
+ {
142
+ image: { url: imageUrl },
143
+ caption,
144
+ ...(mentions.length ? { mentions } : {}),
145
+ },
146
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
147
+ );
148
+ if (sendTextAfterImage && responseText && responseText !== caption) {
149
+ await sendAndStore(sock, remoteJid, { text: responseText, ...(mentions.length ? { mentions } : {}) }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
150
+ }
151
+ return;
152
+ } catch (error) {
153
+ logger.warn('Falha ao enviar imagem do RPG Pokemon. Enviando texto puro.', {
154
+ ownerJid,
155
+ action,
156
+ imageUrl,
157
+ error: error.message,
158
+ });
159
+ }
160
+ }
161
+
162
+ await sendAndStore(sock, remoteJid, { text: responseText, ...(mentions.length ? { mentions } : {}) }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
163
+ } catch (error) {
164
+ logger.error('Erro no comando RPG Pokemon.', {
165
+ ownerJid,
166
+ action,
167
+ error: error.message,
168
+ });
169
+
170
+ await sendAndStore(sock, remoteJid, { text: '❌ Erro ao executar comando RPG. Tente novamente em instantes.' }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
171
+ }
172
+ };
@@ -0,0 +1,192 @@
1
+ export const BIOME_KEYS = ['floresta', 'cidade', 'caverna'];
2
+
3
+ export const BIOME_DEFINITIONS = {
4
+ floresta: {
5
+ key: 'floresta',
6
+ label: 'Floresta',
7
+ preferredTypes: ['grass', 'bug'],
8
+ preferredHabitats: ['forest', 'grassland'],
9
+ },
10
+ cidade: {
11
+ key: 'cidade',
12
+ label: 'Cidade',
13
+ preferredTypes: ['electric', 'normal'],
14
+ preferredHabitats: ['urban', 'rough-terrain'],
15
+ },
16
+ caverna: {
17
+ key: 'caverna',
18
+ label: 'Caverna',
19
+ preferredTypes: ['rock', 'ground'],
20
+ preferredHabitats: ['cave', 'mountain'],
21
+ },
22
+ };
23
+
24
+ export const MISSION_KEYS = {
25
+ EXPLORE: 'explorar',
26
+ WIN: 'vitorias',
27
+ CAPTURE: 'capturas',
28
+ };
29
+
30
+ export const DAILY_MISSION_TARGET = {
31
+ [MISSION_KEYS.EXPLORE]: 3,
32
+ [MISSION_KEYS.WIN]: 2,
33
+ [MISSION_KEYS.CAPTURE]: 1,
34
+ };
35
+
36
+ export const WEEKLY_MISSION_TARGET = {
37
+ [MISSION_KEYS.EXPLORE]: 20,
38
+ [MISSION_KEYS.WIN]: 12,
39
+ [MISSION_KEYS.CAPTURE]: 5,
40
+ };
41
+
42
+ export const DAILY_MISSION_REWARD = {
43
+ gold: 150,
44
+ xp: 120,
45
+ items: [{ key: 'potion', quantity: 1 }],
46
+ };
47
+
48
+ export const WEEKLY_MISSION_REWARD = {
49
+ gold: 800,
50
+ xp: 500,
51
+ items: [
52
+ { key: 'superpotion', quantity: 2 },
53
+ { key: 'pokeball', quantity: 2 },
54
+ ],
55
+ };
56
+
57
+ const toInt = (value, fallback = 0) => {
58
+ const parsed = Number(value);
59
+ return Number.isFinite(parsed) ? Math.trunc(parsed) : fallback;
60
+ };
61
+
62
+ const toDateOnly = (value) => {
63
+ if (!value) return null;
64
+ if (value instanceof Date) return value.toISOString().slice(0, 10);
65
+ return String(value).slice(0, 10);
66
+ };
67
+
68
+ const stableHash = (value) => {
69
+ const raw = String(value || '');
70
+ let hash = 0;
71
+ for (let i = 0; i < raw.length; i += 1) {
72
+ hash = (hash * 31 + raw.charCodeAt(i)) >>> 0;
73
+ }
74
+ return hash;
75
+ };
76
+
77
+ export const resolveBiomeFromKey = (biomeKey) => {
78
+ const key = String(biomeKey || '')
79
+ .trim()
80
+ .toLowerCase();
81
+ return BIOME_DEFINITIONS[key] || null;
82
+ };
83
+
84
+ export const resolveDefaultBiomeForGroup = (groupJid) => {
85
+ const index = stableHash(groupJid) % BIOME_KEYS.length;
86
+ const biomeKey = BIOME_KEYS[index];
87
+ return BIOME_DEFINITIONS[biomeKey];
88
+ };
89
+
90
+ export const resolveMissionRefs = (date = new Date()) => {
91
+ const year = date.getUTCFullYear();
92
+ const month = date.getUTCMonth();
93
+ const day = date.getUTCDate();
94
+ const today = new Date(Date.UTC(year, month, day));
95
+ const weekDay = today.getUTCDay();
96
+ const diffToMonday = weekDay === 0 ? 6 : weekDay - 1;
97
+ const monday = new Date(today);
98
+ monday.setUTCDate(today.getUTCDate() - diffToMonday);
99
+
100
+ return {
101
+ dailyRefDate: toDateOnly(today),
102
+ weeklyRefDate: toDateOnly(monday),
103
+ };
104
+ };
105
+
106
+ export const buildMissionProgressZero = () => ({
107
+ [MISSION_KEYS.EXPLORE]: 0,
108
+ [MISSION_KEYS.WIN]: 0,
109
+ [MISSION_KEYS.CAPTURE]: 0,
110
+ });
111
+
112
+ export const normalizeMissionProgress = (value) => {
113
+ const source = value && typeof value === 'object' ? value : {};
114
+ return {
115
+ [MISSION_KEYS.EXPLORE]: Math.max(0, toInt(source[MISSION_KEYS.EXPLORE], 0)),
116
+ [MISSION_KEYS.WIN]: Math.max(0, toInt(source[MISSION_KEYS.WIN], 0)),
117
+ [MISSION_KEYS.CAPTURE]: Math.max(0, toInt(source[MISSION_KEYS.CAPTURE], 0)),
118
+ };
119
+ };
120
+
121
+ export const isMissionCompleted = (progress, target) => {
122
+ return progress[MISSION_KEYS.EXPLORE] >= target[MISSION_KEYS.EXPLORE] && progress[MISSION_KEYS.WIN] >= target[MISSION_KEYS.WIN] && progress[MISSION_KEYS.CAPTURE] >= target[MISSION_KEYS.CAPTURE];
123
+ };
124
+
125
+ export const resolveMissionStateForRefs = ({ ownerJid, row, refs }) => {
126
+ const normalized = {
127
+ owner_jid: ownerJid,
128
+ daily_ref_date: toDateOnly(row?.daily_ref_date) || refs.dailyRefDate,
129
+ weekly_ref_date: toDateOnly(row?.weekly_ref_date) || refs.weeklyRefDate,
130
+ daily_progress_json: normalizeMissionProgress(row?.daily_progress_json),
131
+ weekly_progress_json: normalizeMissionProgress(row?.weekly_progress_json),
132
+ daily_claimed_at: row?.daily_claimed_at || null,
133
+ weekly_claimed_at: row?.weekly_claimed_at || null,
134
+ };
135
+
136
+ let dirty = false;
137
+ if (normalized.daily_ref_date !== refs.dailyRefDate) {
138
+ normalized.daily_ref_date = refs.dailyRefDate;
139
+ normalized.daily_progress_json = buildMissionProgressZero();
140
+ normalized.daily_claimed_at = null;
141
+ dirty = true;
142
+ }
143
+
144
+ if (normalized.weekly_ref_date !== refs.weeklyRefDate) {
145
+ normalized.weekly_ref_date = refs.weeklyRefDate;
146
+ normalized.weekly_progress_json = buildMissionProgressZero();
147
+ normalized.weekly_claimed_at = null;
148
+ dirty = true;
149
+ }
150
+
151
+ return {
152
+ normalized,
153
+ dirty,
154
+ };
155
+ };
156
+
157
+ export const resolveVictoryRewards = (battleSnapshot) => {
158
+ const enemyLevel = Math.max(1, toInt(battleSnapshot?.enemy?.level, 1));
159
+ const growthRate = String(battleSnapshot?.enemy?.growthRate || '')
160
+ .trim()
161
+ .toLowerCase();
162
+ const isGymBattle = battleSnapshot?.mode === 'gym';
163
+
164
+ const growthMultiplier = growthRate === 'slow' ? 1.12 : growthRate === 'fast' ? 0.92 : growthRate === 'medium slow' ? 1.06 : 1;
165
+
166
+ const rewards = {
167
+ playerXp: Math.max(1, Math.round(enemyLevel * 14 * growthMultiplier)),
168
+ pokemonXp: Math.max(1, Math.round(enemyLevel * 20 * growthMultiplier)),
169
+ gold: Math.max(40, Math.round(enemyLevel * 12 * growthMultiplier)),
170
+ items: [],
171
+ };
172
+
173
+ if (isGymBattle) {
174
+ rewards.playerXp = Math.round(rewards.playerXp * 2.2);
175
+ rewards.pokemonXp = Math.round(rewards.pokemonXp * 2);
176
+ rewards.gold = Math.round(rewards.gold * 2.3);
177
+ rewards.items.push({ key: 'pokeball', quantity: 1 });
178
+ }
179
+
180
+ return rewards;
181
+ };
182
+
183
+ export const __testablesRpgPokemonDomain = {
184
+ resolveBiomeFromKey,
185
+ resolveDefaultBiomeForGroup,
186
+ resolveMissionRefs,
187
+ buildMissionProgressZero,
188
+ normalizeMissionProgress,
189
+ isMissionCompleted,
190
+ resolveMissionStateForRefs,
191
+ resolveVictoryRewards,
192
+ };
@@ -0,0 +1,93 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { __testablesRpgPokemonDomain } from './rpgPokemonDomain.js';
5
+
6
+ const { resolveBiomeFromKey, resolveDefaultBiomeForGroup, resolveMissionRefs, buildMissionProgressZero, normalizeMissionProgress, isMissionCompleted, resolveMissionStateForRefs, resolveVictoryRewards } = __testablesRpgPokemonDomain;
7
+
8
+ test('bioma deve ser resolvido por chave e mapeamento de grupo deve ser determinístico', () => {
9
+ const cidade = resolveBiomeFromKey('cidade');
10
+ assert.equal(cidade?.key, 'cidade');
11
+ assert.deepEqual(cidade?.preferredTypes, ['electric', 'normal']);
12
+
13
+ const first = resolveDefaultBiomeForGroup('120363000000000000@g.us');
14
+ const second = resolveDefaultBiomeForGroup('120363000000000000@g.us');
15
+ assert.equal(first?.key, second?.key);
16
+ });
17
+
18
+ test('missões: referência diária/semanal deve usar dia UTC e início da semana na segunda', () => {
19
+ const refs = resolveMissionRefs(new Date(Date.UTC(2026, 1, 10, 15, 30, 0)));
20
+ assert.equal(refs.dailyRefDate, '2026-02-10');
21
+ assert.equal(refs.weeklyRefDate, '2026-02-09');
22
+
23
+ const sundayRefs = resolveMissionRefs(new Date(Date.UTC(2026, 1, 8, 5, 0, 0)));
24
+ assert.equal(sundayRefs.dailyRefDate, '2026-02-08');
25
+ assert.equal(sundayRefs.weeklyRefDate, '2026-02-02');
26
+ });
27
+
28
+ test('missões: reset diário/semanal deve limpar progresso e claimed quando muda referência', () => {
29
+ const refs = {
30
+ dailyRefDate: '2026-02-10',
31
+ weeklyRefDate: '2026-02-09',
32
+ };
33
+
34
+ const row = {
35
+ daily_ref_date: '2026-02-09',
36
+ daily_progress_json: { explorar: 3, vitorias: 2, capturas: 1 },
37
+ daily_claimed_at: '2026-02-09T20:00:00.000Z',
38
+ weekly_ref_date: '2026-02-02',
39
+ weekly_progress_json: { explorar: 20, vitorias: 12, capturas: 5 },
40
+ weekly_claimed_at: '2026-02-05T20:00:00.000Z',
41
+ };
42
+
43
+ const result = resolveMissionStateForRefs({
44
+ ownerJid: 'player@s.whatsapp.net',
45
+ row,
46
+ refs,
47
+ });
48
+
49
+ assert.equal(result.dirty, true);
50
+ assert.deepEqual(result.normalized.daily_progress_json, buildMissionProgressZero());
51
+ assert.deepEqual(result.normalized.weekly_progress_json, buildMissionProgressZero());
52
+ assert.equal(result.normalized.daily_claimed_at, null);
53
+ assert.equal(result.normalized.weekly_claimed_at, null);
54
+ });
55
+
56
+ test('missões: normalização e conclusão devem refletir metas preenchidas', () => {
57
+ const normalized = normalizeMissionProgress({
58
+ explorar: '3',
59
+ vitorias: 2,
60
+ capturas: 1,
61
+ });
62
+
63
+ assert.deepEqual(normalized, {
64
+ explorar: 3,
65
+ vitorias: 2,
66
+ capturas: 1,
67
+ });
68
+
69
+ assert.equal(
70
+ isMissionCompleted(normalized, {
71
+ explorar: 3,
72
+ vitorias: 2,
73
+ capturas: 1,
74
+ }),
75
+ true,
76
+ );
77
+ });
78
+
79
+ test('ginásio deve ter recompensa superior e item bônus em relação à batalha selvagem', () => {
80
+ const wild = resolveVictoryRewards({
81
+ mode: 'wild',
82
+ enemy: { level: 10 },
83
+ });
84
+ const gym = resolveVictoryRewards({
85
+ mode: 'gym',
86
+ enemy: { level: 10 },
87
+ });
88
+
89
+ assert.ok(gym.playerXp > wild.playerXp);
90
+ assert.ok(gym.pokemonXp > wild.pokemonXp);
91
+ assert.ok(gym.gold > wild.gold);
92
+ assert.deepEqual(gym.items, [{ key: 'pokeball', quantity: 1 }]);
93
+ });
@@ -0,0 +1,46 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { registerEvolutionPokedexEntry } from './rpgEvolutionUtils.js';
5
+
6
+ test('atualização de Pokédex deve ser ignorada quando não há evolução válida', async () => {
7
+ let called = 0;
8
+ const registerEntry = async () => {
9
+ called += 1;
10
+ };
11
+
12
+ const changed = await registerEvolutionPokedexEntry({
13
+ ownerJid: '5511999999999@s.whatsapp.net',
14
+ evolutionOutcome: null,
15
+ registerEntry,
16
+ });
17
+
18
+ assert.equal(changed, false);
19
+ assert.equal(called, 0);
20
+ });
21
+
22
+ test('atualização de Pokédex deve registrar pokeId evoluído', async () => {
23
+ const calls = [];
24
+ const registerEntry = async (payload, connection) => {
25
+ calls.push({ payload, connection });
26
+ };
27
+
28
+ const changed = await registerEvolutionPokedexEntry({
29
+ ownerJid: '5511888888888@s.whatsapp.net',
30
+ evolutionOutcome: {
31
+ updatePayload: {
32
+ pokeId: 6,
33
+ },
34
+ },
35
+ connection: { tx: true },
36
+ registerEntry,
37
+ });
38
+
39
+ assert.equal(changed, true);
40
+ assert.equal(calls.length, 1);
41
+ assert.deepEqual(calls[0].payload, {
42
+ ownerJid: '5511888888888@s.whatsapp.net',
43
+ pokeId: 6,
44
+ });
45
+ assert.deepEqual(calls[0].connection, { tx: true });
46
+ });