Skip to content
Merged
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
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ FROM deps AS build
COPY . .
RUN npm run build # -> dist/
RUN mkdir -p dist/fonts && cp -r src/fonts/. dist/fonts/
RUN mkdir -p dist/assets && cp -r src/assets/. dist/assets/

FROM node:20-bookworm-slim AS run
WORKDIR /app
Expand Down
59 changes: 33 additions & 26 deletions bun.lock

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions migrations/1761962620020_stat-background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;

/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const up = (pgm) => {
pgm.addColumn('users', { stat_background: { type: 'varchar(255)', notNull: false, default: 'bgMain.png' }});
};

/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {
pgm.dropColumn('users', 'stat_background');
};
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,8 @@
"prettier": "^3.6.2",
"ts-node": "^10.9.2",
"typescript": "^5.9.2"
}
},
"trustedDependencies": [
"skia-canvas"
]
}
3 changes: 2 additions & 1 deletion src/@types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,14 @@ declare module 'psqlDB' {
mmr: number
peak_mmr: number
win_streak: number
stat_background: string
stats: {
label: string
value: string
percentile: number
isTop: boolean
}[]
previous_games: { change: number; time: Date }[]
previous_games: { change: number; time: Date; deck: string; stake: string }[]
elo_graph_data: { date: Date; rating: number }[]
rank_name?: string | null
rank_color?: string | null
Expand Down
Binary file added src/assets/BlackBL.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/BlackBR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/BlackTL.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/BlackTR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/BlueBL.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/BlueBR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/BlueTL.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/BlueTR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/GrayBL.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/GrayBR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/GrayTL.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/GrayTR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/HideBL.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/HideBR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/HideTL.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/HideTR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/RedBL.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/RedBR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/RedTR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/antiBL.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/antiBR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/antiTL.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/antiTR.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/backgrounds/bgAbandoned.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/backgrounds/bgAnaglyph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/backgrounds/bgBlack.png
Binary file added src/assets/backgrounds/bgBlue.png
Binary file added src/assets/backgrounds/bgCheckered.png
Binary file added src/assets/backgrounds/bgCocktail.png
Binary file added src/assets/backgrounds/bgErratic.png
Binary file added src/assets/backgrounds/bgFelt.png
Binary file added src/assets/backgrounds/bgGhost.png
Binary file added src/assets/backgrounds/bgGreen.png
Binary file added src/assets/backgrounds/bgMagic.png
Binary file added src/assets/backgrounds/bgMain.png
Binary file added src/assets/backgrounds/bgNebula.png
Binary file added src/assets/backgrounds/bgOrange.png
Binary file added src/assets/backgrounds/bgPainted.png
Binary file added src/assets/backgrounds/bgPlanet.png
Binary file added src/assets/backgrounds/bgPlasma.png
Binary file added src/assets/backgrounds/bgRed.png
Binary file added src/assets/backgrounds/bgViolet.png
Binary file added src/assets/backgrounds/bgYellow.png
Binary file added src/assets/backgrounds/bgZodiac.png
Binary file added src/assets/bgBL.png
Binary file added src/assets/bgBR.png
Binary file added src/assets/bgTL.png
Binary file added src/assets/bgTR.png
Binary file added src/assets/bonus.png
Binary file added src/assets/glass.png
Binary file added src/assets/gold.png
Binary file added src/assets/lucky.png
Binary file added src/assets/mult.png
Binary file added src/assets/redTL.png
Binary file added src/assets/standard.png
Binary file added src/assets/steel.png
Binary file added src/assets/stone.png
Binary file added src/assets/wild.png
72 changes: 72 additions & 0 deletions src/commands/queues/setStatsBackground.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
ChatInputCommandInteraction,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
ActionRowBuilder,
AttachmentBuilder,
MessageFlags,
} from 'discord.js'
import { BACKGROUNDS, getBackgroundById } from '../../utils/backgroundManager'
import { pool } from '../../db'
import { Canvas } from 'skia-canvas'
import path from 'path'
import { loadImage } from 'skia-canvas'

export default {
async execute(interaction: ChatInputCommandInteraction) {
try {
// Create select menu with all backgrounds
const selectMenu = new StringSelectMenuBuilder()
.setCustomId('stats-background-select')
.setPlaceholder('Choose background for your stats card!')
.addOptions(
BACKGROUNDS.map((bg) =>
new StringSelectMenuOptionBuilder()
.setLabel(bg.name)
.setValue(bg.id)
.setDescription(`Set ${bg.name} as your stats background`),
),
)

const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
selectMenu,
)

await interaction.reply({
content: 'Select a background for your stats card:',
components: [row],
flags: MessageFlags.Ephemeral,
})
} catch (error: any) {
console.error('Error showing background selector:', error)
await interaction.reply({
content: `Failed to show background selector: ${error.message}`,
flags: MessageFlags.Ephemeral,
})
}
},
}

// Helper function to generate a preview of the background
export async function generateBackgroundPreview(
backgroundFilename: string,
): Promise<AttachmentBuilder> {
const scale = 2
const width = 800
const height = 600

const canvas = new Canvas(width * scale, height * scale)
const ctx = canvas.getContext('2d')

ctx.scale(scale, scale)
ctx.imageSmoothingEnabled = false

// Draw background
const bg = await loadImage(
path.join(__dirname, '../../assets/backgrounds', backgroundFilename),
)
ctx.drawImage(bg, 0, 0)

const buffer = await canvas.toBuffer('png', { quality: 1.0, density: scale })
return new AttachmentBuilder(buffer, { name: 'preview.png' })
}
9 changes: 7 additions & 2 deletions src/commands/queues/statsQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@ export default {

const queueName = interaction.options.getString('queue-name', true)
const targetUser = interaction.options.getUser('user') || interaction.user
const byDate =
interaction.options.getString('by-date') === 'yes' ? true : false
const queueId = await getQueueIdFromName(queueName)
const playerStats = await getStatsCanvasUserData(targetUser.id, queueId)
const statFile = await drawPlayerStatsCanvas(queueName, playerStats)
const statFile = await drawPlayerStatsCanvas(
queueName,
playerStats,
byDate,
)
const viewStatsButtons = setupViewStatsButtons(queueName)

await interaction.editReply({
Expand All @@ -24,7 +30,6 @@ export default {
})

// Update queue role, just to be sure it's correct when they check
// This is a nice bandaid fix till we update leaderboard roles better
await setUserQueueRole(queueId, targetUser.id)
} catch (err: any) {
console.error(err)
Expand Down
9 changes: 9 additions & 0 deletions src/commands/superCommands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import setDefaultDeckBans from '../queues/setDefaultDeckBans'
import setPriorityQueue from '../queues/setPriorityQueue'
import queue from './queue'
import setStatsBackground from '../queues/setStatsBackground'

export default {
data: new SlashCommandBuilder()
Expand Down Expand Up @@ -39,13 +40,21 @@ export default {
.setRequired(true)
.setAutocomplete(true),
),
)

.addSubcommand((sub) =>
sub
.setName('stats-background')
.setDescription('Choose a background for your stats card'),
),

async execute(interaction: ChatInputCommandInteraction) {
if (interaction.options.getSubcommand() === 'priority-queue') {
await setPriorityQueue.execute(interaction)
} else if (interaction.options.getSubcommand() === 'preset-deck-bans') {
await setDefaultDeckBans.execute(interaction)
} else if (interaction.options.getSubcommand() === 'stats-background') {
await setStatsBackground.execute(interaction)
}
},

Expand Down
7 changes: 7 additions & 0 deletions src/commands/superCommands/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ export default {
.setName('user')
.setDescription('The user to view stats for (defaults to yourself)')
.setRequired(false),
)
.addStringOption((option) =>
option
.setName('by-date')
.setDescription('Sort the stats by date')
.addChoices([{ name: 'yes', value: 'yes' }])
.setRequired(false),
),
),
async execute(interaction: ChatInputCommandInteraction) {
Expand Down
53 changes: 49 additions & 4 deletions src/events/interactionCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ import {
handleVoting,
} from '../utils/voteHelpers'
import { drawPlayerStatsCanvas } from '../utils/canvasHelpers'
import { getBackgroundById } from '../utils/backgroundManager'
import { generateBackgroundPreview } from '../commands/queues/setStatsBackground'

// Track users currently processing queue joins to prevent duplicates
const processingQueueJoins = new Set<string>()
Expand Down Expand Up @@ -214,6 +216,45 @@ export default {
}
}

if (interaction.customId === 'stats-background-select') {
try {
await interaction.deferUpdate()

const selectedId = interaction.values[0]
const background = getBackgroundById(selectedId)

if (!background) {
await interaction.followUp({
content: 'Invalid background selected.',
flags: MessageFlags.Ephemeral,
})
return
}

const previewImage = await generateBackgroundPreview(
background.filename,
)

// Update user's background in database
await pool.query(
'UPDATE users SET stat_background = $1 WHERE user_id = $2',
[background.filename, interaction.user.id],
)

await interaction.followUp({
content: `Background set to **${background.name}**!\n\nHere's a preview of your stats background:`,
files: [previewImage],
flags: MessageFlags.Ephemeral,
})
} catch (error: any) {
console.error('Error setting background:', error)
await interaction.followUp({
content: `Failed to set background: ${error.message}`,
flags: MessageFlags.Ephemeral,
})
}
}

if (interaction.values[0].includes('winmatch_')) {
const customSelId = interaction.values[0]
const matchId = parseInt(customSelId.split('_')[1])
Expand Down Expand Up @@ -538,23 +579,27 @@ export default {

if (interaction.customId.startsWith('view-stats-')) {
try {
await interaction.deferReply()
const queueName = interaction.customId.split('-')[2]
const queueId = await getQueueIdFromName(queueName)
const playerStats = await getStatsCanvasUserData(
interaction.user.id,
queueId,
)
const statFile = await drawPlayerStatsCanvas(queueName, playerStats)
const statFile = await drawPlayerStatsCanvas(
queueName,
playerStats,
false,
)
const viewStatsButtons = setupViewStatsButtons(queueName)

await interaction.reply({
await interaction.editReply({
files: [statFile],
components: [viewStatsButtons],
})
} catch (err) {
await interaction.reply({
await interaction.editReply({
content: `You don't have any stats for this queue.`,
flags: [MessageFlags.Ephemeral],
})
}
}
Expand Down
Binary file added src/fonts/m6x11.ttf
Binary file not shown.
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ client.on('error', (error: Error) => {
console.error('Stack:', error.stack)
})

// Preload background images
import { preloadBackgrounds } from './utils/backgroundManager'
void preloadBackgrounds().catch((error) => {
console.error('[BACKGROUND PRELOAD ERROR]', error)
})

void client.login(token).catch((error) => {
console.error('[LOGIN FAILED]', error)
process.exit(1)
Expand Down
75 changes: 75 additions & 0 deletions src/utils/backgroundManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { loadImage } from 'skia-canvas'
import path from 'path'

// Background metadata
export interface Background {
id: string
name: string
filename: string
}

// Cache for preloaded images
const imageCache = new Map<string, any>()

// List of all available backgrounds
export const BACKGROUNDS: Background[] = [
{ id: 'abandoned', name: 'Abandoned', filename: 'bgAbandoned.png' },
{ id: 'anaglyph', name: 'Anaglyph', filename: 'bgAnaglyph.png' },
{ id: 'black', name: 'Black', filename: 'bgBlack.png' },
{ id: 'blue', name: 'Blue', filename: 'bgBlue.png' },
{ id: 'checkered', name: 'Checkered', filename: 'bgCheckered.png' },
{ id: 'cocktail', name: 'Cocktail', filename: 'bgCocktail.png' },
{ id: 'erratic', name: 'Erratic', filename: 'bgErratic.png' },
{ id: 'felt', name: 'Felt', filename: 'bgFelt.png' },
{ id: 'ghost', name: 'Ghost', filename: 'bgGhost.png' },
{ id: 'green', name: 'Green', filename: 'bgGreen.png' },
{ id: 'magic', name: 'Magic', filename: 'bgMagic.png' },
{ id: 'main', name: 'Main', filename: 'bgMain.png' },
{ id: 'nebula', name: 'Nebula', filename: 'bgNebula.png' },
{ id: 'orange', name: 'Orange', filename: 'bgOrange.png' },
{ id: 'painted', name: 'Painted', filename: 'bgPainted.png' },
{ id: 'planet', name: 'Planet', filename: 'bgPlanet.png' },
{ id: 'plasma', name: 'Plasma', filename: 'bgPlasma.png' },
{ id: 'red', name: 'Red', filename: 'bgRed.png' },
{ id: 'violet', name: 'Violet', filename: 'bgViolet.png' },
{ id: 'yellow', name: 'Yellow', filename: 'bgYellow.png' },
{ id: 'zodiac', name: 'Zodiac', filename: 'bgZodiac.png' },
]

// Preload all background images
export async function preloadBackgrounds(): Promise<void> {
console.log('Preloading background images...')

for (const bg of BACKGROUNDS) {
try {
const imagePath = path.join(
__dirname,
'../assets/backgrounds',
bg.filename,
)
const image = await loadImage(imagePath)
imageCache.set(bg.filename, image)
} catch (error) {
console.error(`Failed to load ${bg.filename}:`, error)
}
}

console.log(`Preloaded ${imageCache.size}/${BACKGROUNDS.length} backgrounds`)
}

// Get a preloaded background image
export function getBackground(filename: string): any | null {
return imageCache.get(filename) || null
}

// Get background by ID
export function getBackgroundById(id: string): Background | undefined {
return BACKGROUNDS.find((bg) => bg.id === id)
}

// Get background by filename
export function getBackgroundByFilename(
filename: string,
): Background | undefined {
return BACKGROUNDS.find((bg) => bg.filename === filename)
}
Loading