diff --git a/Dockerfile b/Dockerfile index 5ce52a6..c51fe78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,8 +14,7 @@ RUN yarn build-prod FROM node:17-alpine WORKDIR /usr/src/bot/ COPY src/ ./ -COPY CascadiaCode.ttf ./ -COPY hotTakeData.json ./ +COPY static/ ./ COPY --from=build /usr/src/bot/node_modules ./node_modules/ COPY --from=build /usr/src/bot/bin ./bin/ COPY --from=build /usr/src/bot/package.json ./package.json diff --git a/src/Config.prod.ts b/src/Config.prod.ts index 9d9cfac..4618fb6 100644 --- a/src/Config.prod.ts +++ b/src/Config.prod.ts @@ -45,7 +45,11 @@ export const config: Config = { }, branding: { color: '#C6BFF7', - font: 'CascadiaCode.ttf', + fonts: { + cascadia: 'static/Cascadia/CascadiaCode.ttf', + montserratBold: 'static/Montserrat/Montserrat-Bold.ttf', + montserratSemiBold: 'static/Montserrat/Montserrat-SemiBold.ttf' + }, welcomeMessage: member => `Welcome ${mention(member)} to the Developer Den!\nCurrent Member Count: ${member.guild.memberCount}` }, diff --git a/src/Config.ts b/src/Config.ts index ae9642b..55519af 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -34,7 +34,11 @@ export const config: Config = { pastebin: prodConfig.pastebin, branding: { color: '#ffffff', - font: 'CascadiaCode.ttf', + fonts: { + cascadia: 'static/Cascadia/CascadiaCode.ttf', + montserratBold: 'static/Montserrat/Montserrat-Bold.ttf', + montserratSemiBold: 'static/Montserrat/Montserrat-SemiBold.ttf' + }, welcomeMessage: member => `Welcome ${mention(member)} to the Developer Den test server!\nCurrent Member Count: ${member.guild.memberCount}` }, diff --git a/src/modules/hotTakes/hotTakes.util.ts b/src/modules/hotTakes/hotTakes.util.ts index 7acc34d..a736536 100644 --- a/src/modules/hotTakes/hotTakes.util.ts +++ b/src/modules/hotTakes/hotTakes.util.ts @@ -12,7 +12,7 @@ const hotTakeData: { problems: string[], tlds: string[] takes: string[], -} = JSON.parse(readFileSync(process.cwd() + '/hotTakeData.json').toString()) +} = JSON.parse(readFileSync(process.cwd() + '/static/Storage/hotTakeData.json').toString()) const placeholders = { language: () => hotTakeData.languages, diff --git a/src/modules/xp/leaderboard.command.ts b/src/modules/xp/leaderboard.command.ts index 2ed27c9..2020cc9 100644 --- a/src/modules/xp/leaderboard.command.ts +++ b/src/modules/xp/leaderboard.command.ts @@ -1,4 +1,4 @@ -import {CommandInteraction, GuildMember} from 'discord.js' +import {CommandInteraction, GuildMember, User} from 'discord.js' import { APIApplicationCommandOptionChoice @@ -8,8 +8,10 @@ import {Command} from 'djs-slash-helper' import {ApplicationCommandOptionType, ApplicationCommandType} from 'discord-api-types/v10' import {createStandardEmbed} from '../../util/embeds.js' import {branding} from '../../util/branding.js' -import {actualMention} from '../../util/users.js' import {getActualDailyStreak} from './dailyReward.command.js' +import {fonts, getCanvasContext} from '../../util/imageUtils.js' +import { drawText } from '../../util/textRendering.js' +import {loadImage} from 'canvas' interface LeaderboardType extends APIApplicationCommandOptionChoice { calculate?: (user: DDUser) => Promise, @@ -19,6 +21,12 @@ interface LeaderboardType extends APIApplicationCommandOptionChoice { value: keyof DDUser } +type LeaderboardData = { + name: string; + value: string; + avatar: string; +} + const info: LeaderboardType[] = [ {value: 'xp', name: 'XP', format: value => `${value} XP`}, {value: 'level', name: 'Level', format: value => `Level ${value}`}, @@ -64,31 +72,142 @@ export const LeaderboardCommand: Command = { const calculate = traitInfo.calculate ?? ((user: DDUser) => Promise.resolve(user[value])) const users = await DDUser.findAll({ order: [[value, 'DESC']], - limit: 10 + limit: 3 }).then(users => users.filter(async it => await calculate(it) > 0)) if (users.length == 0) { await interaction.followUp('No applicable users') return } - const embed = { - ...createStandardEmbed(interaction.member as GuildMember), - title: `${branding.name} Leaderboard`, - description: `The top ${users.length} users based on ${name}`, - fields: await Promise.all(users.map(async (user, index) => { - const discordUser = await guild.client.users.fetch(user.id.toString()).catch(() => null) - return { - name: `#${index + 1} - ${format(await calculate(user))}`, - value: discordUser == null ? 'Unknown User' : actualMention(discordUser) - } - })) - } - await interaction.followUp({embeds: [embed]}) + + const data = await Promise.all(users.map(async (user) => { + const discordUser = await guild.members.fetch(user.id.toString()).catch(() => null) + const value = format(await calculate(user)) + + if (discordUser == null) return null + + return { + name: discordUser.displayName, + value: value, + avatar: discordUser.displayAvatarURL({ extension: 'png' }) + } + })) + + const member = (interaction.options.getMember('member') ?? interaction.member) as GuildMember + const image = await createLeaderboardImage(traitInfo, data as LeaderboardData[]) + + await interaction.followUp({ + embeds: [ + createStandardEmbed(member) + .setTitle(`${branding.name} Leaderboard - ${name}`) + .setImage('attachment://leaderboard.png') + ], + files: [{ attachment: image.toBuffer(), name: 'leaderboard.png' }] + }) } } +async function createLeaderboardImage(type: LeaderboardType, [first, second, third]: LeaderboardData[]) { + const [canvas, ctx] = getCanvasContext(1000, 500) + + const background = await loadImage('static/Pictures/leaderboardBackground.png') + ctx.drawImage(background, 0, 0) + + const goldAvatar = await loadImage(first.avatar) + const silverAvatar = await loadImage(second.avatar) + const bronzeAvatar = await loadImage(third.avatar) + + ctx.drawImage(goldAvatar, 457, 108, 85, 85) + ctx.drawImage(silverAvatar, 152, 158, 85, 85) + ctx.drawImage(bronzeAvatar, 762, 208, 85, 85) + ctx.drawImage(background, 0, 0) + + drawText(ctx, first.value, fonts.montserratSemiBold, { + x: 405, + y: 448, + width: 190, + height: 40 + }, { + hAlign: 'center', + vAlign: 'center', + maxSize: 70, + minSize: 1, + granularity: 3 + }) + + drawText(ctx, first.name, fonts.montserratBold, { + x: 405, + y: 213, + width: 190, + height: 40 + }, { + hAlign: 'center', + vAlign: 'center', + maxSize: 25, + minSize: 1, + granularity: 3 + }) + + drawText(ctx, second.value, fonts.montserratSemiBold, { + x: 99, + y: 448, + width: 190, + height: 40 + }, { + hAlign: 'center', + vAlign: 'center', + maxSize: 70, + minSize: 1, + granularity: 3 + }) + + drawText(ctx, second.name, fonts.montserratBold, { + x: 99, + y: 263, + width: 190, + height: 40 + }, { + hAlign: 'center', + vAlign: 'center', + maxSize: 25, + minSize: 1, + granularity: 3 + }) + + drawText(ctx, third.value, fonts.montserratSemiBold, { + x: 710, + y: 448, + width: 190, + height: 40 + }, { + hAlign: 'center', + vAlign: 'center', + maxSize: 70, + minSize: 1, + granularity: 3 + }) + + drawText(ctx, third.name, fonts.montserratBold, { + x: 710, + y: 312, + width: 190, + height: 40 + }, { + hAlign: 'center', + vAlign: 'center', + maxSize: 25, + minSize: 1, + granularity: 3 + }) + + + return canvas +} + function formatDays(days: number) { if (days == 1) { return '1 day' } return `${days} days` } + + diff --git a/src/modules/xp/xp.command.ts b/src/modules/xp/xp.command.ts index 0bc2ecf..bf798e1 100644 --- a/src/modules/xp/xp.command.ts +++ b/src/modules/xp/xp.command.ts @@ -2,7 +2,7 @@ import {CommandInteraction, GuildMember, User} from 'discord.js' import {getUserById} from '../../store/models/DDUser.js' import {createStandardEmbed} from '../../util/embeds.js' import {xpForLevel} from './xpForMessage.util.js' -import {createImage, font, getCanvasContext} from '../../util/imageUtils.js' +import {createImage, fonts, getCanvasContext} from '../../util/imageUtils.js' import {branding} from '../../util/branding.js' import {drawText} from '../../util/textRendering.js' import {Command} from 'djs-slash-helper' @@ -66,7 +66,7 @@ function createXpImage(xp: number, user: GuildMember) { ctx.fillStyle = user.roles?.color?.hexColor ?? branding.color const message = `${xp.toLocaleString()} XP` - drawText(ctx, message, font, { + drawText(ctx, message, fonts.cascadia, { x: 0, y: 0, width: canvas.width, diff --git a/src/util/branding.ts b/src/util/branding.ts index ba55c43..beb8df0 100644 --- a/src/util/branding.ts +++ b/src/util/branding.ts @@ -5,7 +5,7 @@ export type BrandingConfig = { name?: string, iconUrl?: string, welcomeMessage: (member: GuildMember | PartialGuildMember) => string - font: string, + fonts: { cascadia: string, montserratBold: string, montserratSemiBold: string }, color: string } diff --git a/src/util/imageUtils.ts b/src/util/imageUtils.ts index 948603b..ceaf454 100644 --- a/src/util/imageUtils.ts +++ b/src/util/imageUtils.ts @@ -4,7 +4,11 @@ import {branding} from './branding.js' const {createCanvas} = canvas -export const font = loadSync(branding.font) +export const fonts = { + cascadia: loadSync(branding.fonts.cascadia), + montserratBold: loadSync(branding.fonts.montserratBold), + montserratSemiBold: loadSync(branding.fonts.montserratSemiBold) +} export function createImage(width: number, height: number, color: string): Canvas { const canvas = createCanvas(width, height) diff --git a/CascadiaCode.ttf b/static/Cascadia/CascadiaCode.ttf similarity index 100% rename from CascadiaCode.ttf rename to static/Cascadia/CascadiaCode.ttf diff --git a/static/Montserrat/Montserrat-Bold.ttf b/static/Montserrat/Montserrat-Bold.ttf new file mode 100644 index 0000000..efddc83 Binary files /dev/null and b/static/Montserrat/Montserrat-Bold.ttf differ diff --git a/static/Montserrat/Montserrat-SemiBold.ttf b/static/Montserrat/Montserrat-SemiBold.ttf new file mode 100644 index 0000000..cbf44db Binary files /dev/null and b/static/Montserrat/Montserrat-SemiBold.ttf differ diff --git a/static/Pictures/leaderboardBackground.png b/static/Pictures/leaderboardBackground.png new file mode 100644 index 0000000..4e8e07a Binary files /dev/null and b/static/Pictures/leaderboardBackground.png differ diff --git a/hotTakeData.json b/static/Storage/hotTakeData.json similarity index 100% rename from hotTakeData.json rename to static/Storage/hotTakeData.json