diff --git a/.gitignore b/.gitignore index 27941d6..b85cde1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ *giveaway* .sern /generated/generated/prisma +src/utils/db/dict.db \ No newline at end of file diff --git a/TODO.md b/TODO.md index b13520d..a61bed7 100644 --- a/TODO.md +++ b/TODO.md @@ -10,7 +10,7 @@ - [x] /a - Custom command with user autocomplete ## Miscellaneous Commands -- [ ] /rolemenu - Role selection menu (owner only) +- [x] /rolemenu - Role selection menu (owner only) - [ ] /creditos - Bot credits and acknowledgments - [x] ~~/infinitecraft - InfiniteCraft recipe solver~~ - [ ] /letra - Song lyrics search via Genius API diff --git a/bun.lock b/bun.lock index 63411fe..4a39205 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,7 @@ }, "devDependencies": { "@sern/cli": "^1.4.0", + "@types/bun": "^1.2.18", "@types/mongodb": "^4.0.7", "@types/node": "^17.0.25", "prisma": "^6.10.1", @@ -189,12 +190,16 @@ "@sern/publisher": ["@sern/publisher@1.1.2", "", {}, "sha512-1zh99JZykKUhqHhE75ZXfiLsBtf1WI+NnDCojv8UlpnGBEyzO8xyI1X7PNf6cPKRs4W9XqY3PqTJ+hrqzIsMkg=="], + "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + "@types/luxon": ["@types/luxon@3.4.2", "", {}, "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA=="], "@types/mongodb": ["@types/mongodb@4.0.7", "", { "dependencies": { "mongodb": "*" } }, "sha512-lPUYPpzA43baXqnd36cZ9xxorprybxXDzteVKCPAdp14ppHtFJHnXYvNpmBvtMUTb5fKXVv6sVbzo1LHkWhJlw=="], "@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="], + "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], "@types/whatwg-url": ["@types/whatwg-url@11.0.5", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ=="], @@ -223,6 +228,8 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], @@ -255,6 +262,8 @@ "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], diff --git a/package.json b/package.json index 2143710..1696dca 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "devDependencies": { "@sern/cli": "^1.4.0", + "@types/bun": "^1.2.18", "@types/mongodb": "^4.0.7", "@types/node": "^17.0.25", "prisma": "^6.10.1", diff --git a/src/commands/silly/chiste.ts b/src/commands/silly/chiste.ts index e4e959e..cbf5bce 100644 --- a/src/commands/silly/chiste.ts +++ b/src/commands/silly/chiste.ts @@ -23,7 +23,8 @@ export default commandModule({ ? 'EL CHISTE DEL PINGUINO OMAIGAD' : null ) - .setDescription(joke.text); + // regex matches both '-' and '- ' and escapes it because of discord markdown + .setDescription(joke.text.replace(/^-\s?/gm, '\\- ')); await ctx.reply({ embeds: [embed] }); }, diff --git a/src/commands/silly/hangman.ts b/src/commands/silly/hangman.ts new file mode 100644 index 0000000..376ed5e --- /dev/null +++ b/src/commands/silly/hangman.ts @@ -0,0 +1,104 @@ +import { Palabra } from '#/db/dict.types'; +import { WordController } from '#/wordController'; +import { commandModule, CommandType } from '@sern/handler'; +import { ActionRowBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; + +const helpText = ` +## whar +se tiene en cuenta la cabeza, el cuerpo, pierna izquierda, pierna derecha, brazo izquierdo y brazo derecho. +## quiero palabras que no sean 6 letras +talk is cheap, send patches +## porque la palabra es tan rara +Las palabras son como espejos del alma colectiva - lo que nos parece extraño revela más sobre nosotros que sobre la palabra misma. En el juego del ahorcado, como en la vida, enfrentamos lo desconocido con la esperanza de que nuestras conjeturas nos acerquen a la verdad. Cada palabra "rara" es una invitación a expandir los límites de nuestro entendimiento. +`; + +export default commandModule({ + type: CommandType.Slash, + plugins: [], + description: 'hang, man', + //alias : [], + execute: async (ctx, sdt) => { + const dict = sdt.deps.dict; + const { palabra } = (await dict + .query('select palabra from palabra where length(palabra) = 6 order by random() limit 1') + .get()) as Record<'palabra', Palabra['palabra']>; + + const wordcon = new WordController(palabra, ctx); + let lastSubmitUserId = ''; + + const collector = (await wordcon.getMessage())!.createMessageComponentCollector({ + time: 180_000, // 3 minutes + }); + collector.on('collect', async (interaction) => { + if (interaction.customId === 'hangman-answer') { + if (interaction.user.id !== lastSubmitUserId) { + lastSubmitUserId = interaction.user.id; + } else { + if (process.env.NODE_ENV === 'development') return; + const i = await interaction.reply({ + content: 'Ya has respondido, espera a que se acabe el tiempo.\nEsto se elimina en 2 segundos, no te preocupes por cerrarlo.', + ephemeral: true, + }); + setTimeout(() => i.delete().catch(() => { }), 2000); + return; + } + + await interaction.showModal({ + title: 'Responde', + customId: 'hangman-answer-modal', + components: [ + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId('hangman-answer-input') + .setLabel('Tu respuesta') + .setStyle(TextInputStyle.Short) + ), + ], + }); + const submitted = await interaction.awaitModalSubmit({ + time: 30_000, + filter: (i) => i.user.id === interaction.user.id, + }); + wordcon.submitLetter( + submitted.fields.getTextInputValue('hangman-answer-input').toLowerCase() + ); + await submitted.deferUpdate(); + } else if (interaction.customId === 'hangman-help') { + await interaction.reply({ content: helpText, ephemeral: true }); + } + }); + + const gameOver = async (won = false) => { + (await wordcon.getMessage())!.edit({ + content: `la palabra era \`${palabra}\`. ${won ? 'enhorabuena!' : 'vaya hombre...'}`, + components: [], + }); + collector.stop(); + }; + const editMsgContent = async () => { + const msg = await wordcon.getMessage(); + if (msg) { + await msg.edit({ + content: `**Incorrectas:** ${wordcon.data.incorrect.join(', ')}\n**Correctas:** ${wordcon.data.correct.join(', ')}\nÚltima letra enviada por ${lastSubmitUserId ? `<@${lastSubmitUserId}>` : 'nadie'}`, + allowedMentions: { users: [] }, + }); + } + }; + + collector.on('end', async (_, reason) => { + await gameOver(reason === 'time' ? false : true); + }); + wordcon.on('correct', async () => { + if (wordcon.data.correct.length === palabra.length) { + return await gameOver(true); + } + await editMsgContent(); + }); + wordcon.on('incorrect', async () => { + await editMsgContent(); + }); + wordcon.on('gameOver', async () => { + await gameOver(); + }); + }, +}); diff --git a/src/commands/silly/palabra.ts b/src/commands/silly/palabra.ts new file mode 100644 index 0000000..3586181 --- /dev/null +++ b/src/commands/silly/palabra.ts @@ -0,0 +1,16 @@ +import { Palabra } from '#/db/dict.types'; +import { commandModule, CommandType } from '@sern/handler'; + +export default commandModule({ + type: CommandType.Slash, + plugins: [], + description: 'palabra.ts', + //alias : [], + execute: async (ctx, sdt) => { + const dict = sdt.deps.dict; + const { palabra } = (await dict + .query('select palabra from palabra order by random() limit 1') + .get()) as Record<'palabra', Palabra['palabra']>; + await ctx.reply(palabra); + }, +}); diff --git a/src/dependencies.d.ts b/src/dependencies.d.ts index 31ab35a..249966d 100644 --- a/src/dependencies.d.ts +++ b/src/dependencies.d.ts @@ -8,17 +8,18 @@ import type { CoreDependencies } from '@sern/handler'; import type { Client } from 'discord.js'; import type { Publisher } from '@sern/publisher'; import type prisma from './utils/db/index.js'; +import type { Database } from 'bun:sqlite'; /** * Note: You usually would not need to modify this unless there is an urgent need to break the contracts provided. * You would need to modify this to add your custom Services, however. */ declare global { - interface Dependencies extends CoreDependencies { - '@sern/client': Client; - 'publisher': Publisher; - 'prisma': prisma; - } + interface Dependencies extends CoreDependencies { + '@sern/client': Client; + publisher: Publisher; + prisma: prisma; + dict: Database; + } } - -export {} +export {}; diff --git a/src/index.ts b/src/index.ts index 5f1d9f8..bc4aa79 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import * as config from './config.js' import { Client, GatewayIntentBits } from 'discord.js'; import { Sern, makeDependencies } from '@sern/handler'; import { Publisher } from '@sern/publisher' +import { Database } from 'bun:sqlite'; import prisma from './utils/db/index.js'; const client = new Client({ intents: [ @@ -27,6 +28,7 @@ await makeDependencies(({ add }) => { deps['@sern/logger']! )); add('prisma', prisma); + add('dict', new Database('src/utils/db/dict.db')) }); //View docs for all options diff --git a/src/utils/db/dict.types.ts b/src/utils/db/dict.types.ts new file mode 100644 index 0000000..ddd3668 --- /dev/null +++ b/src/utils/db/dict.types.ts @@ -0,0 +1,13 @@ +export interface Palabra { + palabra: string; + tipo: string; + 'género': string; + 'número': string; + 'raíz': string; + afijo: string; + 'tónica': string; + 'sílabas': string; + locale: string; + origen: string; + 'sinónimos': string[]; +} \ No newline at end of file diff --git a/src/utils/wordController.ts b/src/utils/wordController.ts new file mode 100644 index 0000000..19dc506 --- /dev/null +++ b/src/utils/wordController.ts @@ -0,0 +1,156 @@ +// hangman word controller +// gnu gpl 3 by @SrIzan10 +// coords for most stuff in canvas by claude 7 sonnet because i do not like canvas + +import { createCanvas, type SKRSContext2D } from "@napi-rs/canvas"; +import type { Context } from "@sern/handler"; +import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, type Message } from "discord.js"; +import { EventEmitter } from "node:events"; + +export class WordController extends EventEmitter { + private word: string; + private canvasCtx = createCanvas(500, 500).getContext('2d'); + private ctx: Context; + private msg?: Message; + + private incorrectLetters: string[] = []; + private correctLetters: string[] = []; + + constructor(word: string, ctx: Context) { + super(); + this.word = word.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); // bye accents + this.ctx = ctx; + } + + public get data() { + return { + incorrect: this.incorrectLetters, + correct: this.correctLetters, + word: this.word, + }; + } + + public async getMessage() { + if (!this.msg) { + await this.updateMsg(); + } + return this.msg; + } + + public async submitLetter(letter: string) { + if (letter.length !== 1 || !/^[a-z]$/i.test(letter)) { + letter = letter.toLowerCase().trim()[0]; + } + if (this.incorrectLetters.includes(letter) || this.correctLetters.includes(letter)) { + return; + } + + if (!this.word.includes(letter)) { + this.incorrectLetters.push(letter); + this.emit('incorrect', letter); + } else { + this.correctLetters.push(letter); + this.emit('correct', letter); + } + + await this.updateMsg(); + } + + public async updateMsg() { + this.canvasUpdate(); + const file = await this.canvasCtx.canvas.encode('webp'); + const attachment = new AttachmentBuilder(file, { name: 'hangman.webp' }); + + if (!this.msg) { + const buttons = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId('hangman-answer').setLabel('Responde').setEmoji('✅').setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId('hangman-help').setLabel('Ayuda').setEmoji('❓').setStyle(ButtonStyle.Secondary), + ); + this.msg = await this.ctx.reply({ files: [attachment], components: [buttons] }); + return this.msg; + } + + return this.msg.edit({ files: [attachment] }); + } + + private canvasUpdate() { + this.canvasCtx.fillStyle = '#1e1e2e'; + this.canvasCtx.fillRect(0, 0, this.canvasCtx.canvas.width, this.canvasCtx.canvas.height); + + this.canvasCtx.strokeStyle = '#cdd6f4'; + this.canvasCtx.lineWidth = 2; + this.canvasCtx.beginPath(); + this.canvasCtx.moveTo(150, 320); + this.canvasCtx.lineTo(150, 70); + this.canvasCtx.lineTo(250, 70); + this.canvasCtx.lineTo(250, 100); + this.canvasCtx.stroke(); + + if (this.incorrectLetters.length >= 1) this.writeHead(); + if (this.incorrectLetters.length >= 2) this.writeBody(); + if (this.incorrectLetters.length >= 3) this.writeLeg('left'); + if (this.incorrectLetters.length >= 4) this.writeLeg('right'); + if (this.incorrectLetters.length >= 5) this.writeArm('left'); + if (this.incorrectLetters.length >= 6) { + this.writeArm('right'); + this.emit('gameOver', this.word); + }; + + const wordLength = this.word.length; + const startX = 250 - (wordLength * 20); + const baseY = 450; + + this.canvasCtx.font = '20px Arial'; + this.canvasCtx.textAlign = 'center'; + this.canvasCtx.fillStyle = '#cdd6f4'; + this.canvasCtx.strokeStyle = '#cdd6f4'; + this.canvasCtx.lineWidth = 2; + + for (let i = 0; i < wordLength; i++) { + const letter = this.word[i]; + const letterX = startX + i * 40; + + this.canvasCtx.beginPath(); + this.canvasCtx.moveTo(letterX - 15, baseY + 5); + this.canvasCtx.lineTo(letterX + 15, baseY + 5); + this.canvasCtx.stroke(); + + if (this.correctLetters.includes(letter.toLowerCase())) { + this.canvasCtx.fillText(letter.toUpperCase(), letterX, baseY - 10); + } + } + + if (process.env.NODE_ENV === 'development') { + this.canvasCtx.font = '20px'; + this.canvasCtx.fillStyle = '#a6adc8'; + this.canvasCtx.fillText(`answer is: ${this.word}`, 250, 30); + } + } + + private writeHead() { + this.canvasCtx.beginPath(); + this.canvasCtx.arc(250, 115, 15, 0, 2 * Math.PI); + this.canvasCtx.stroke(); + } + + private writeBody() { + this.canvasCtx.beginPath(); + this.canvasCtx.moveTo(250, 130); + this.canvasCtx.lineTo(250, 200); + this.canvasCtx.stroke(); + } + + private writeLeg(type: 'left' | 'right') { + this.canvasCtx.beginPath(); + this.canvasCtx.moveTo(250, 200); + this.canvasCtx.lineTo(type === 'left' ? 225 : 275, 240); + this.canvasCtx.stroke(); + } + + private writeArm(type: 'left' | 'right') { + this.canvasCtx.beginPath(); + this.canvasCtx.moveTo(250, 155); + this.canvasCtx.lineTo(type === 'left' ? 225 : 275, 180); + this.canvasCtx.stroke(); + } +} \ No newline at end of file