From 651db565fba4fc98d77a8a3747bf127e4af9381a Mon Sep 17 00:00:00 2001 From: JustDams Date: Mon, 25 Nov 2024 21:24:25 +0000 Subject: [PATCH] WIP: default map card --- commands/map.js | 103 +++++++++++++++++- config.json | 19 ++++ functions/customType.js | 7 +- functions/graph.js | 71 ++++++++++-- .../selectmenus/mapRadarChartSelector.js | 0 languages/base.js | 2 + languages/en-US/translations.js | 3 + languages/setup.js | 1 + templates/customType.js | 3 + templates/errorCard.js | 11 +- 10 files changed, 201 insertions(+), 19 deletions(-) create mode 100644 interactions/selectmenus/mapRadarChartSelector.js diff --git a/commands/map.js b/commands/map.js index 0b1dd04d..2e2a5772 100644 --- a/commands/map.js +++ b/commands/map.js @@ -10,9 +10,85 @@ const { getTranslations, getTranslation } = require('../languages/setup') const { getStats } = require('../functions/apiHandler') const errorCard = require('../templates/errorCard') const { getInteractionOption, getGameOption } = require('../functions/utility') +const { generateButtons } = require('../functions/customType') +const { TYPES } = require('../templates/customType') + +const ALL = 'all' + +const buildDefaultMapEmbed = async (interaction, playerId, game, mode, types = null) => { + types ??= [ + TYPES.MATCHES, + TYPES.WINRATE, + { + ...TYPES.ELO_GAIN, + style: Discord.ButtonStyle.Secondary + } + ] + + const matchNumber = types.find(t => t.name === TYPES.ELO_GAIN.name)?.style === Discord.ButtonStyle.Secondary ? 20 : 0 + const { + playerDatas, + playerStats, + playerHistory + } = await getStats({ + playerParam: { + param: playerId, + faceitId: true, + }, + matchNumber, + checkElo: true, + game + }) + + const faceitLevel = playerDatas.games[game].skill_level + const faceitElo = playerDatas.games[game].faceit_elo + const size = 40 + const filesAtt = [] + const values = { + id: 'mapRadarChartSelector', + playerId, + userId: interaction.user.id, + game, + mode, + map: ALL, + types + } + + const buttons = await Promise.all(types.map(t => generateButtons(interaction, values, t))) + + const rankImageCanvas = await Graph.getRankImage(faceitLevel, faceitElo, size, game) + playerStats.segments.forEach(s => { + const eloGain = playerHistory + .filter(h => (h.i1 === maps[s.label] || h.i1 === s.label) && s.mode === mode && h.gameMode === s.mode) + .map(h => h.eloGain).filter(h => h).reduce((acc, h) => acc + h, 0) + + s.stats['Elo Gain'] = eloGain + }) + playerStats.segments.sort((a, b) => a.label.localeCompare(b.label)) + const radarChartCanvas = Graph.getMapRadarChart(playerStats.segments, types.filter(t => t.style !== Discord.ButtonStyle.Secondary)) + filesAtt.push(new Discord.AttachmentBuilder(radarChartCanvas, { name: 'radar.png' })) + filesAtt.push(new Discord.AttachmentBuilder(rankImageCanvas, { name: 'level.png' })) + + const embed = new Discord.EmbedBuilder() + .setAuthor({ name: playerDatas.nickname, iconURL: playerDatas.avatar || null, url: `https://www.faceit.com/en/players/${playerDatas.nickname}` }) + .setDescription(`[Steam](https://steamcommunity.com/profiles/${playerDatas.games[game].game_player_id}), [Faceit](https://www.faceit.com/en/players/${playerDatas.nickname})`) + .setColor(color.levels[game][faceitLevel].color) + .setThumbnail('attachment://level.png') + .setImage('attachment://radar.png') + + const actionRow = new Discord.ActionRowBuilder() + .addComponents(buttons) + + return { + embeds: [embed], + components: [actionRow], + files: filesAtt + } +} const buildEmbed = async (interaction, playerId, map, mode, game) => { if (!map) return + if (map === ALL) return buildDefaultMapEmbed(interaction, playerId, game, mode) const { playerDatas, @@ -89,7 +165,7 @@ const buildEmbed = async (interaction, playerId, map, mode, game) => { } } -const sendCardWithInfo = async (interaction, playerParam, map = null, mode = null, game = null) => { +const sendCardWithInfo = async (interaction, playerParam, map = ALL, mode = null, game = null) => { map ??= getInteractionOption(interaction, 'map') game ??= getGameOption(interaction) mode ??= '5v5' @@ -108,17 +184,33 @@ const sendCardWithInfo = async (interaction, playerParam, map = null, mode = nul if (!playerStats.segments.length) throw getTranslation('error.user.noMatches', interaction.locale, { playerName: playerDatas.nickname }) const playerId = playerDatas.player_id + const defaultValues = { + playerId, + userId: interaction.user.id, + game, + } + let content = getTranslation('strings.selectMapDescription', interaction.locale, { playerName: playerDatas.nickname }) let options = [] - const totalMatches = playerStats.segments.reduce((acc, e) => acc + parseInt(e.stats.Matches), 0) + const defaultId = (await Interaction.create({ + ...defaultValues, + map: ALL, + mode: '5v5' + })).id + const defaultOption = new Discord.StringSelectMenuOptionBuilder() + .setLabel(getTranslation('strings.allMaps', interaction.locale)) + .setDescription(getTranslation('strings.allMapsDescription', interaction.locale)) + .setValue(defaultId) + .setDefault(true) + options.push(defaultOption) + + const totalMatches = playerStats.segments.reduce((acc, e) => acc + parseInt(e.stats.Matches), 0) await Promise.all(playerStats.segments.map(async (e) => { e.label = game === 'cs2' ? maps[e.label] : e.label const label = `${e.label} ${e.mode}` const values = { - playerId, - userId: interaction.user.id, - game, + ...defaultValues, map: e.label, mode: e.mode } @@ -158,6 +250,7 @@ const sendCardWithInfo = async (interaction, playerParam, map = null, mode = nul const resp = await buildEmbed(interaction, playerId, map, mode, game) embeds = resp.embeds files = resp.files + if (resp.components) components.push(...resp.components) content = '' } diff --git a/config.json b/config.json index 5e7dbe50..1c87c521 100644 --- a/config.json +++ b/config.json @@ -147,6 +147,25 @@ "color": "#6B9B46" } }, + "charts": { + "text": "#c9d1d9", + "grid": "#3c3c3c", + "background": "#2f3136", + "radarCategories": { + "Matches": { + "border": "rgb(76, 191, 192)", + "background": "rgba(76, 191, 192, 0.2)" + }, + "Win Rate %": { + "border": "rgb(54, 162, 235)", + "background": "rgba(54, 162, 235, 0.2)" + }, + "Elo Gain": { + "border": "rgb(153, 102, 255)", + "background": "rgba(153, 102, 255, 0.2)" + } + } + }, "error": "#770000" }, "emojis": { diff --git a/functions/customType.js b/functions/customType.js index baa75bd3..0c173535 100644 --- a/functions/customType.js +++ b/functions/customType.js @@ -28,12 +28,15 @@ const generateButtons = async (interaction, values, type, disabledType = null) = userId: interaction.user.id }))).id - return new ButtonBuilder() + const button = new ButtonBuilder() .setCustomId(customId) .setLabel(name) - .setEmoji(type.emoji) .setStyle(type.style) .setDisabled(type.name === disabledType?.name) + + if (type.emoji) button.setEmoji(type.emoji) + + return button } const updateButtons = (components, type, jsonData = null) => { diff --git a/functions/graph.js b/functions/graph.js index c455323f..bf9095d6 100644 --- a/functions/graph.js +++ b/functions/graph.js @@ -26,17 +26,16 @@ const getChart = (datasets, labels, datasetFunc, displayY1, game) => { const canvas = Canvas.createCanvas(600, 400) const ctx = canvas.getContext('2d') - const color = '#c9d1d9', gridColor = '#3c3c3c' const yAxisBase = { border: { width: 1, }, grid: { - color: gridColor, + color: color.charts.grid, }, ticks: { beginAtZero: false, - color: color, + color: color.charts.text, } } @@ -60,17 +59,17 @@ const getChart = (datasets, labels, datasetFunc, displayY1, game) => { }, x: { grid: { - color: gridColor, + color: color.charts.grid, }, ticks: { - color: color, + color: color.charts.text, } } }, plugins: { legend: { labels: { - color: color, + color: color.charts.text, borderWidth: 1, } } @@ -81,7 +80,7 @@ const getChart = (datasets, labels, datasetFunc, displayY1, game) => { const ctx = chart.canvas.getContext('2d') ctx.save() ctx.globalCompositeOperation = 'source-over' - ctx.fillStyle = '#2f3136' + ctx.fillStyle = color.charts.background ctx.fillRect(0, 0, chart.width, chart.height) ctx.restore() } @@ -223,6 +222,61 @@ const getGraph = (locale, playerName, type, matchHistory, maxMatch) => { } } +const getMapRadarChart = (segments, types) => { + const canvas = Canvas.createCanvas(600, 600) + const ctx = canvas.getContext('2d') + const datasetsKeys = types.map(e => e.name) + const datasets = datasetsKeys.map(key => { + return { + label: key, + data: segments.map(e => e.stats[key]), + backgroundColor: color.charts.radarCategories[key].background, + borderColor: color.charts.radarCategories[key].border, + borderWidth: 2 + } + }) + + new Chart(ctx, { + type: 'radar', + data: { + labels: segments.map(e => e.label), + datasets: datasets + }, + options: { + scales: { + r: { + grid: { + color: color.charts.grid, + }, + ticks: { + color: color.charts.text, + backdropColor: color.charts.background, + }, + backgroundColor: color.charts.background, + pointLabels: { + display: true, + font: { + size: 16 + }, + color: color.charts.text, + padding: 10 + } + } + }, + plugins: { + legend: { + labels: { + color: color.charts.text, + borderWidth: 2, + } + } + } + } + }) + + return canvas.toBuffer() +} + module.exports = { generateChart, getRankImage, @@ -231,5 +285,6 @@ module.exports = { getChart, getGraph, getCompareDatasets, - getEloGain + getEloGain, + getMapRadarChart } diff --git a/interactions/selectmenus/mapRadarChartSelector.js b/interactions/selectmenus/mapRadarChartSelector.js new file mode 100644 index 00000000..e69de29b diff --git a/languages/base.js b/languages/base.js index 850bf2cb..8b02d010 100644 --- a/languages/base.js +++ b/languages/base.js @@ -160,6 +160,8 @@ module.exports = { verifyDescription: '', verify: '', premiumDesc: '', + allMaps: '', + allMapsDescription: '', }, error: { user: { diff --git a/languages/en-US/translations.js b/languages/en-US/translations.js index fa03036b..0d9d7416 100644 --- a/languages/en-US/translations.js +++ b/languages/en-US/translations.js @@ -120,6 +120,9 @@ base.strings.pagination = { } base.strings.donate = 'Support the project' base.strings.fullHistory = 'Full history' +base.strings.allMaps = 'All maps' +// Radar chart +base.strings.allMapsDescription = 'Sneak peek of different maps statistics' base.strings.premiumDesc = 'This feature is only available for premium guilds. Become a premium guild by clicking on the button below' base.error.user.missing = 'It seems like there is a user missing' base.error.user.compareSame = 'You can\'t compare the same user' diff --git a/languages/setup.js b/languages/setup.js index 47e48bd0..2418e366 100644 --- a/languages/setup.js +++ b/languages/setup.js @@ -20,6 +20,7 @@ const getTranslation = (key, language, replace) => { try { languageConf = require(`./${language}/translations`) string = languageConf[key] + if (!string.length) throw new Error('Empty string') } catch (error) { languageConf = require('./en-US/translations') string = languageConf[key] diff --git a/templates/customType.js b/templates/customType.js index 35ef90fc..a4cb9018 100644 --- a/templates/customType.js +++ b/templates/customType.js @@ -9,6 +9,9 @@ module.exports.TYPES = { PREV: { name: 'strings.pagination.prev', emoji: '◀', style: ButtonStyle.Primary, translate: true }, LAST: { name: 'strings.pagination.last', emoji: '⏭', style: ButtonStyle.Primary, translate: true }, FIRST: { name: 'strings.pagination.first', emoji: '⏮', style: ButtonStyle.Primary, translate: true }, + MATCHES: { name: 'Matches', style: ButtonStyle.Success, translate: false }, + WINRATE: { name: 'Win Rate %', style: ButtonStyle.Success, translate: false }, + ELO_GAIN: { name: 'Elo Gain', style: ButtonStyle.Success, translate: false }, } module.exports.getType = (t) => Object.entries(this.TYPES).filter(e => e[1].name.normalize() === t)[0][1] diff --git a/templates/errorCard.js b/templates/errorCard.js index e80e062c..bbaa1136 100644 --- a/templates/errorCard.js +++ b/templates/errorCard.js @@ -6,9 +6,12 @@ module.exports = (description, lang) => { if (typeof description === 'string' && keyReg.test(description)) description = getTranslation(description, lang) return { - embeds: [new Discord.EmbedBuilder() - .setColor(color.error) - .setDescription(description ?? getTranslation('error.execution.command', lang)) - .setFooter({ text: `${name} ${getTranslation('strings.error', lang)}` })] + embeds: [ + new Discord.EmbedBuilder() + .setColor(color.error) + .setDescription(description ?? getTranslation('error.execution.command', lang)) + .setFooter({ text: `${name} ${getTranslation('strings.error', lang)}` }) + ], + files: [] } } \ No newline at end of file