Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sexy leaderboard #51

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/Config.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
},
Expand Down
6 changes: 5 additions & 1 deletion src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
},
Expand Down
2 changes: 1 addition & 1 deletion src/modules/hotTakes/hotTakes.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
151 changes: 135 additions & 16 deletions src/modules/xp/leaderboard.command.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {CommandInteraction, GuildMember} from 'discord.js'
import {CommandInteraction, GuildMember, User} from 'discord.js'

import {
APIApplicationCommandOptionChoice
Expand All @@ -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<string> {
calculate?: (user: DDUser) => Promise<number>,
Expand All @@ -19,6 +21,12 @@ interface LeaderboardType extends APIApplicationCommandOptionChoice<string> {
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}`},
Expand Down Expand Up @@ -64,31 +72,142 @@ export const LeaderboardCommand: Command<ApplicationCommandType.ChatInput> = {
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`
}


4 changes: 2 additions & 2 deletions src/modules/xp/xp.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/util/branding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
6 changes: 5 additions & 1 deletion src/util/imageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
File renamed without changes.
Binary file added static/Montserrat/Montserrat-Bold.ttf
Binary file not shown.
Binary file added static/Montserrat/Montserrat-SemiBold.ttf
Binary file not shown.
Binary file added static/Pictures/leaderboardBackground.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.