From c1be21b9a492bb68f0d6aca529c47a710ccf938f Mon Sep 17 00:00:00 2001 From: baz Date: Mon, 2 Dec 2024 01:05:47 +0000 Subject: [PATCH] Add Wrapped command --- commands/100-games/wrapped.js | 361 ++++++++++++++++++++++++++++++++++ databaseHelperFunctions.js | 31 ++- 2 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 commands/100-games/wrapped.js diff --git a/commands/100-games/wrapped.js b/commands/100-games/wrapped.js new file mode 100644 index 0000000..2a16378 --- /dev/null +++ b/commands/100-games/wrapped.js @@ -0,0 +1,361 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); +const { getUserRegistration, getBeatenGames, checkGameStorageId, getChangelog, getLeaderboardEntriesBetweenDates, getLeaderboardEntries } = require('../../databaseHelperFunctions'); +const { getGameJson, getGenres, getCompanyInfo } = require('../../igdbHelperFunctions'); + +let userBeatenGamesDatabaseEntries = {}; +let beatGameIGDBEntries = []; +let companies = []; + +module.exports = { + data: new SlashCommandBuilder() + .setName('wrapped') + .setDescription('Get your 2024 summary.') + .addUserOption(option => option.setName('user').setDescription('The user to check')), + async execute(interaction) { + + await interaction.deferReply(); + + let user = interaction.user; + const userOption = interaction.options.getUser('user'); + + if (userOption) { + user = userOption; + } + + const userDatabaseEntry = await getUserRegistration(user); + if (!userDatabaseEntry) return interaction.followUp({ content: `Issue checking registration with "${user.username}".`, ephemeral: true }); + + await GetBeatenGamesForYear(userDatabaseEntry, 2024); + + if (!userBeatenGamesDatabaseEntries || userBeatenGamesDatabaseEntries.length === 0) { + return interaction.followUp({ content: `${user.username} hasn't beaten any games in this time frame.`, ephemeral: false }); + } + + await GetIGDBEntries(userBeatenGamesDatabaseEntries); + await GetDevelopers(); + + const numberOfGamesBeat = await GetNumberOfGamesBeat(); + const averageBeatGameInterval = await GetAverageBeatInterval(); + const mostActiveMonth = await GetMostActiveMonth(); + const oldestGameBeat = await GetOldestGameBeat(); + const newestGameBeat = await GetNewestGameBeat(); + const averageGameAge = await GetAverageGameAge(); + const favouriteGameGenres = await GetFavouriteGenres(); + const favouriteGameDevs = await GetFavouriteDevelopers(); + const favouriteGamePublishers = await GetFavouritePublishers(); + const numberOfGamesDropped = await GetNumberOfDroppedGames(userDatabaseEntry); + const yearLeaderboardPlace = await GetYearLeaderboardPosition(userDatabaseEntry); + const currentLeaderboardPlace = await GetLeaderboardPosition(userDatabaseEntry); + + const embed = new EmbedBuilder(); + embed.setColor(0xD36918); + embed.setTitle('The 100 Games Challenge: 2024 Wrapped'); + embed.setDescription(`Here are the stats for the games you played for the 100 Games Challenge in 2024 ${user.displayName}`); + embed.setThumbnail(user.avatarURL()); + embed.setTimestamp(); + + embed.addFields({ name: 'Number of Games Beat', value: numberOfGamesBeat.toString(), inline: true }); + embed.addFields({ name: 'Average Beat Interval', value: `${Math.round(averageBeatGameInterval).toString()} days`, inline: true }); + embed.addFields({ name: 'Most Active Month', value: mostActiveMonth, inline: true }); + embed.addFields({ name: 'Number Of Games Dropped', value: numberOfGamesDropped, inline: true }); + embed.addFields({ name: 'Yearly Leaderboard', value: yearLeaderboardPlace, inline: true }); + embed.addFields({ name: 'Overall Leaderboard', value: currentLeaderboardPlace, inline: true }); + embed.addFields({ name: 'Oldest Beat Game', value: oldestGameBeat }); + embed.addFields({ name: 'Newest Beat Game', value: newestGameBeat }); + embed.addFields({ name: 'Average Game Age', value: averageGameAge }); + embed.addFields({ name: 'Favourite Game Genres', value: favouriteGameGenres }); + embed.addFields({ name: 'Favourite Game Developers', value: favouriteGameDevs }); + embed.addFields({ name: 'Favourite Game Publisher', value: favouriteGamePublishers }); + + return interaction.followUp({ embeds: [embed] }); + }, +}; + +function FilterByMonth(array, targetMonth) { + return array.filter(entry => { + const date = new Date(entry.updatedAt); + return date.getMonth() === targetMonth; + }); +} + +function FilterByYear(array, targetYear) { + return array.filter(entry => { + const date = new Date(entry.updatedAt); + return date.getFullYear() === targetYear; + }); +} + +async function GetBeatenGamesForYear(userDatabaseEntry, year) { + userBeatenGamesDatabaseEntries = await getBeatenGames(userDatabaseEntry.id); + + if (userBeatenGamesDatabaseEntries && userBeatenGamesDatabaseEntries.length > 0) { + userBeatenGamesDatabaseEntries = FilterByYear(userBeatenGamesDatabaseEntries, year); + } +} + +async function GetIGDBEntries(array) { + beatGameIGDBEntries = []; + + for (let i = 0; i < array.length; i++) { + const game = await checkGameStorageId(array[i].gameId); + const json = await getGameJson(String.prototype.concat('where id = ', game.igdb_id, '; fields *;')); + beatGameIGDBEntries.push(json[0]); + } +} + +async function GetDevelopers() { + companies = []; + + for (let i = 0; i < beatGameIGDBEntries.length; i++) { + for (let j = 0; j < beatGameIGDBEntries[i].involved_companies.length; j++) { + + if (!companies.find(item => item.id === beatGameIGDBEntries[i].involved_companies[j])) { + const company = await getCompanyInfo(beatGameIGDBEntries[i].involved_companies[j]); + companies.push(company); + } + } + } +} + +async function GetNumberOfGamesBeat() { + if (userBeatenGamesDatabaseEntries) { + return userBeatenGamesDatabaseEntries.length; + } + + return 0; +} + +async function GetAverageBeatInterval() { + if (userBeatenGamesDatabaseEntries && userBeatenGamesDatabaseEntries.length > 0) { + const today = new Date(); + const start = new Date(2024, 0, 1); + const days = (today - start) / (1000 * 60 * 60 * 24); + const timepergame = days / userBeatenGamesDatabaseEntries.length; + return Math.round(timepergame); + } + + return 0; +} + +async function GetMostActiveMonth() { + if (userBeatenGamesDatabaseEntries && userBeatenGamesDatabaseEntries.length > 0) { + let mostActiveMonth = []; + + for (let i = 0; i < 12; i++) { + const month = FilterByMonth(userBeatenGamesDatabaseEntries, i); + if (month.length > mostActiveMonth.length) { + mostActiveMonth = month; + } + } + + return String.prototype.concat(mostActiveMonth[0].updatedAt.toLocaleDateString('en-GB', { month: 'long' }), ' (', mostActiveMonth.length.toString(), ' games)'); + } + + return ''; +} + +async function GetOldestGameBeat() { + if (beatGameIGDBEntries.length > 0) { + let oldestGame = beatGameIGDBEntries[0]; + + for (let i = 1; i < beatGameIGDBEntries.length; i++) { + if (beatGameIGDBEntries[i].first_release_date < oldestGame.first_release_date) { + oldestGame = beatGameIGDBEntries[i]; + } + } + + const gameName = oldestGame.name; + const gameReleaseDate = new Intl.DateTimeFormat('en-GB', { dateStyle: 'full' }).format(oldestGame.first_release_date * 1000); + return gameName + ' *(' + gameReleaseDate + ')*'; + } + return ''; +} + +async function GetNewestGameBeat() { + if (beatGameIGDBEntries.length > 0) { + let newestGame = beatGameIGDBEntries[0]; + + for (let i = 1; i < beatGameIGDBEntries.length; i++) { + if (beatGameIGDBEntries[i].first_release_date > newestGame.first_release_date) { + newestGame = beatGameIGDBEntries[i]; + } + } + + const gameName = newestGame.name; + const gameReleaseDate = new Intl.DateTimeFormat('en-GB', { dateStyle: 'full' }).format(newestGame.first_release_date * 1000); + return gameName + ' *(' + gameReleaseDate + ')*'; + } + return ''; +} + +async function GetAverageGameAge() { + if (beatGameIGDBEntries.length > 0) { + let averageGameAge = 0; + + for (let i = 0; i < beatGameIGDBEntries.length; i++) { + averageGameAge += beatGameIGDBEntries[i].first_release_date; + } + + averageGameAge /= beatGameIGDBEntries.length; + averageGameAge = new Date(averageGameAge * 1000); + const today = new Date(); + let year = today.getFullYear() - averageGameAge.getFullYear(); + let month = today.getMonth() - averageGameAge.getMonth(); + let day = today.getDate() - averageGameAge.getDate(); + + if (month < 0) { + year--; + month += 12; + } + + if (day < 0) { + month--; + const previousMonth = new Date(today.getFullYear(), today.getMonth(), 0); + day += previousMonth.getDate(); + } + + + return `${year} years, ${month} months, ${day} days old`; + } + return ''; +} + +async function GetFavouriteGenres() { + const genres = []; + const counts = []; + for (let i = 0; i < beatGameIGDBEntries.length; i++) { + for (let j = 0; j < beatGameIGDBEntries[i].genres.length; j++) { + const genre = await getGenres(beatGameIGDBEntries[i].genres[j]); + genres.push(genre); + } + } + + genres.forEach(item => { + counts[item] = (counts[item] || 0) + 1; + }); + + const keys = Object.keys(counts); + + let string = ''; + for (let i = 0; i < keys.length && i < 3; i++) { + const genre = keys[i]; + + if (counts[genre] >= 1) { + string = string.concat(` ${genre} (${counts[genre]} games),`); + } + } + string = string.slice(1, -1); + + return string; +} + +async function GetFavouriteDevelopers() { + const developers = []; + const counts = []; + for (let i = 0; i < beatGameIGDBEntries.length; i++) { + for (let j = 0; j < companies.length; j++) { + if (companies[j].developed) { + const developedGames = companies[j].developed; + if (developedGames.find(item => item === beatGameIGDBEntries[i].id)) { + developers.push(companies[j].name); + } + } + } + } + + developers.forEach(item => { + counts[item] = (counts[item] || 0) + 1; + }); + + const keys = Object.keys(counts); + + let string = ''; + for (let i = 0; i < keys.length && i < 3; i++) { + const developer = keys[i]; + + if (counts[developer] >= 1) { + string = string.concat(` ${developer} (${counts[developer] } games),`); + } + } + string = string.slice(1, -1); + + return string; +} + +async function GetFavouritePublishers() { + const publishers = []; + const counts = []; + for (let i = 0; i < beatGameIGDBEntries.length; i++) { + for (let j = 0; j < companies.length; j++) { + if (companies[j].published) { + const developedGames = companies[j].published; + if (developedGames.find(item => item === beatGameIGDBEntries[i].id)) { + publishers.push(companies[j].name); + } + } + } + } + + publishers.forEach(item => { + counts[item] = (counts[item] || 0) + 1; + }); + + const keys = Object.keys(counts); + + let string = ''; + for (let i = 0; i < keys.length && i < 3; i++) { + const publisher = keys[i]; + + if (counts[publisher] >= 1) { + string = string.concat(` ${publisher} (${counts[publisher] } games),`); + } + } + string = string.slice(1, -1); + + return string; +} + +async function GetNumberOfDroppedGames(userDatabaseEntry) { + const userChangelog = await getChangelog(userDatabaseEntry.id); + const droppedGames = userChangelog.filter(item => item.oldStatus === 'playing' && item.newStatus === null); + + if (droppedGames) { + return droppedGames.length.toString(); + } + + return '0'; +} + +async function GetYearLeaderboardPosition(userDatabaseEntry) { + const leaderboard = await getLeaderboardEntriesBetweenDates('2024-01-01', '2024-12-31'); + const index = leaderboard.findIndex(item => item.username === userDatabaseEntry.username) + 1; + return await appendOrdinalSuffix(index); +} + +async function GetLeaderboardPosition(userDatabaseEntry) { + const leaderboard = await getLeaderboardEntries(); + const index = leaderboard.findIndex(item => item.username === userDatabaseEntry.username) + 1; + return await appendOrdinalSuffix(index); +} + +async function appendOrdinalSuffix(number) { + const lastDigit = number % 10; + const lastTwoDigits = number % 100; + + // Handle special case for 11th, 12th, 13th, etc. + if (lastTwoDigits >= 11 && lastTwoDigits <= 13) { + return number + 'th'; + } + + // Handle the common cases for 1st, 2nd, 3rd + switch (lastDigit) { + case 1: + return number + 'st'; + case 2: + return number + 'nd'; + case 3: + return number + 'rd'; + default: + return number + 'th'; + } +} \ No newline at end of file diff --git a/databaseHelperFunctions.js b/databaseHelperFunctions.js index ac9ed87..0b93d1f 100644 --- a/databaseHelperFunctions.js +++ b/databaseHelperFunctions.js @@ -1,5 +1,6 @@ const { Users, Games, LoggedGames, Changelog } = require ('./dbObjects.js'); const fs = require('fs'); +const { Op } = require('sequelize'); async function checkUserRegistration(user) { @@ -305,6 +306,33 @@ async function getLeaderboardEntries() { return results; } +async function getLeaderboardEntriesBetweenDates(start, end) { + const users = await Users.findAll() + .catch((err) => { + console.log(err); + }); + + const results = []; + + const startDate = new Date(start); + const endDate = new Date(end); + + for (let i = 0; i < users.length; i++) { + const count = await LoggedGames.count({ where: { userId: users[i].id, status: 'beat', statusLastChanged: { [ Op.between ]: [startDate, endDate] } } }); + + const res = await Users.findOne({ where: { id: users[i].id } }) + .catch((err) => { + console.log(err); + }); + const username = res.username; + + const fun = { username, count }; + results.push(fun); + } + + return results; +} + async function getRecentPlanningGameEntry(userId) { return await getRecentGameEntry(userId, 'planning'); } @@ -392,7 +420,7 @@ async function backupDatabase() { } async function getChangelog(id) { - const changelogEntries = await Changelog.findAll({where: {userId: id}, order: [ [ 'updatedAt', 'DESC' ]] }) + const changelogEntries = await Changelog.findAll({ where: { userId: id }, order: [ [ 'updatedAt', 'DESC' ]] }) .catch((err) => { console.log(err); }); @@ -447,4 +475,5 @@ module.exports = { backupDatabase, getChangelog, getAllChangelog, + getLeaderboardEntriesBetweenDates, }; \ No newline at end of file