mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-09 15:19:50 +03:00
399 lines
10 KiB
TypeScript
399 lines
10 KiB
TypeScript
import { GetChartInfoForUser, GetSessionInfo } from "./apiRequests";
|
|
import { ParseTimelineTarget } from "./argParsers";
|
|
import { PrependTachiUrl, TachiServerV1Get } from "./fetchTachi";
|
|
import {
|
|
CreateChartLink,
|
|
CreateChartMap,
|
|
CreateSongMap,
|
|
Entries,
|
|
FormatChartTierlistInfo,
|
|
FormatDate,
|
|
FormatProfileRating,
|
|
FormatScoreData,
|
|
FormatScoreRating,
|
|
GetChartPertinentInfo,
|
|
MillisToSince,
|
|
NumericSOV,
|
|
ONE_HOUR,
|
|
Pluralise,
|
|
UniqueOnKey,
|
|
UppercaseFirst,
|
|
} from "./misc";
|
|
import { BotConfig, ServerConfig } from "../config";
|
|
import { MessageEmbed, Util } from "discord.js";
|
|
import { FormatChart, FormatGame, GetGamePTConfig } from "tachi-common";
|
|
import type { UGPTFolderStat, UGPTFolderTimeline, UGPTStats } from "./returnTypes";
|
|
import type { EmbedFieldData } from "discord.js";
|
|
import type {
|
|
ChartDocument,
|
|
FolderDocument,
|
|
Game,
|
|
ImportDocument,
|
|
integer,
|
|
PBScoreDocument,
|
|
Playtype,
|
|
PublicUserDocument,
|
|
ScoreDocument,
|
|
SessionDocument,
|
|
SongDocument,
|
|
} from "tachi-common";
|
|
|
|
export function CreateEmbed() {
|
|
return new MessageEmbed()
|
|
.setColor(ServerConfig.type === "ktchi" ? "#e61c6e" : "#527acc")
|
|
.setTimestamp();
|
|
}
|
|
|
|
export function CreateImportEmbed(importDoc: ImportDocument) {
|
|
return CreateEmbed()
|
|
.setTitle(
|
|
`Imported ${importDoc.scoreIDs.length} ${Pluralise(
|
|
importDoc.scoreIDs.length,
|
|
"score"
|
|
)}!`
|
|
)
|
|
.addField("Created Sessions", importDoc.createdSessions.length.toString(), true)
|
|
.addField("Errors", importDoc.errors.length.toString(), true)
|
|
.addField(
|
|
"Your Profile",
|
|
`${BotConfig.TACHI_SERVER_LOCATION}/dashboard/users/${importDoc.userID}/games/${importDoc.game}`
|
|
);
|
|
}
|
|
|
|
export function CreateUserEmbed(userDoc: PublicUserDocument) {
|
|
return CreateEmbed()
|
|
.setTitle(`${userDoc.username} (ID: ${userDoc.id})`)
|
|
.setThumbnail(PrependTachiUrl(`/users/${userDoc.id}/pfp`))
|
|
.setDescription(userDoc.status ?? "No status...")
|
|
.addField("Join Date", FormatDate(userDoc.joinDate))
|
|
.setURL(`${BotConfig.TACHI_SERVER_LOCATION}/dashboard/users/${userDoc.username}`);
|
|
}
|
|
|
|
export function CreateGameProfileEmbed(userDoc: PublicUserDocument, ugptStats: UGPTStats) {
|
|
const { game, playtype } = ugptStats.gameStats;
|
|
|
|
const gptConfig = GetGamePTConfig(game, playtype);
|
|
|
|
return CreateEmbed()
|
|
.setTitle(`${userDoc.username} (${FormatGame(game, playtype)})`)
|
|
.setThumbnail(PrependTachiUrl(`/users/${userDoc.id}/pfp`))
|
|
.setDescription(userDoc.status ?? "No status...")
|
|
.addField(
|
|
"Rankings",
|
|
Entries(ugptStats.rankingData)
|
|
.map(
|
|
([k, v]) =>
|
|
`**${UppercaseFirst(k)}**: #${v.ranking}/${v.outOf} (${FormatProfileRating(
|
|
game,
|
|
playtype,
|
|
k,
|
|
ugptStats.gameStats.ratings[k]
|
|
)})`
|
|
)
|
|
.join("\n") || "No Rankings",
|
|
true
|
|
)
|
|
.addField(
|
|
"Classes",
|
|
Entries(ugptStats.gameStats.classes)
|
|
.map(
|
|
([k, v]) =>
|
|
`**${UppercaseFirst(k)}**: ${gptConfig.classHumanisedFormat[k][v]?.display}`
|
|
)
|
|
.join("\n") || "No Classes",
|
|
true
|
|
)
|
|
.addField("Playcount", ugptStats.totalScores.toString())
|
|
.addField(
|
|
"First Play",
|
|
ugptStats.firstScore.timeAchieved !== null
|
|
? `${FormatDate(ugptStats.firstScore.timeAchieved)}
|
|
(${MillisToSince(ugptStats.firstScore.timeAchieved)})`
|
|
: "N/A",
|
|
true
|
|
)
|
|
.addField(
|
|
"Last Played",
|
|
ugptStats.mostRecentScore.timeAchieved !== null
|
|
? `${FormatDate(ugptStats.mostRecentScore.timeAchieved)}
|
|
(${MillisToSince(ugptStats.mostRecentScore.timeAchieved)})`
|
|
: "N/A",
|
|
true
|
|
)
|
|
.setURL(
|
|
`${BotConfig.TACHI_SERVER_LOCATION}/dashboard/users/${userDoc.username}/games/${game}/${playtype}`
|
|
);
|
|
}
|
|
|
|
export async function CreateChartScoresEmbed(
|
|
userDoc: PublicUserDocument,
|
|
game: Game,
|
|
playtype: Playtype,
|
|
chartID: string,
|
|
renderThisScore: ScoreDocument | null
|
|
) {
|
|
const { song, chart, pb } = await GetChartInfoForUser(userDoc.id, chartID, game, playtype);
|
|
|
|
let score: PBScoreDocument | ScoreDocument | null = renderThisScore;
|
|
|
|
// if the user didn't pass a score to be rendered, render their PB.
|
|
if (renderThisScore === null) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
score = pb;
|
|
}
|
|
|
|
const embed = CreateEmbed()
|
|
.setTitle(`${userDoc.username}: ${FormatChart(game, song, chart)}`)
|
|
.setThumbnail(PrependTachiUrl(`/users/${userDoc.id}/pfp`))
|
|
.setURL(CreateChartLink(chart, game));
|
|
|
|
// unecessary OR here to assert to TS that this implies PB can't be null either.
|
|
if (score === null || pb === null) {
|
|
embed.setDescription(`${userDoc.username} has not played this chart.`);
|
|
} else {
|
|
const { scoreStr, lampStr } = FormatScoreData(score);
|
|
|
|
embed
|
|
.addField("Score", scoreStr, true)
|
|
.addField("Lamp", lampStr, true)
|
|
.addField(
|
|
"Ratings",
|
|
Entries(score.calculatedData)
|
|
.map(
|
|
([key, value]) =>
|
|
`${UppercaseFirst(key)}: ${FormatScoreRating(
|
|
game,
|
|
playtype,
|
|
key,
|
|
value
|
|
)}`
|
|
)
|
|
.join("\n")
|
|
)
|
|
.addField(
|
|
"Last Raised",
|
|
score.timeAchieved !== null
|
|
? `${FormatDate(score.timeAchieved)} (${MillisToSince(score.timeAchieved)})`
|
|
: "N/A",
|
|
true
|
|
);
|
|
|
|
// if score is a PB
|
|
if ("rankingData" in score) {
|
|
embed.addField("Ranking", `#**${score.rankingData.rank}**/${score.rankingData.outOf}`);
|
|
} else {
|
|
const { scoreStr, lampStr } = FormatScoreData(pb);
|
|
|
|
if (pb.scoreData.score !== score.scoreData.score) {
|
|
embed.addField("PB Score", scoreStr, true);
|
|
}
|
|
|
|
if (pb.scoreData.lamp !== score.scoreData.lamp) {
|
|
embed.addField("PB Lamp", lampStr, true);
|
|
}
|
|
}
|
|
|
|
const pertinentInfo = GetChartPertinentInfo(game, chart);
|
|
const tierlistInfo = FormatChartTierlistInfo(game, chart);
|
|
|
|
if (pertinentInfo) {
|
|
embed.addField("Related", pertinentInfo, !!tierlistInfo);
|
|
}
|
|
|
|
if (tierlistInfo) {
|
|
embed.addField("Tierlist Info", tierlistInfo, !!pertinentInfo);
|
|
}
|
|
}
|
|
|
|
return embed;
|
|
}
|
|
|
|
export async function CreateFolderStatsEmbed(
|
|
game: Game,
|
|
playtype: Playtype,
|
|
username: string,
|
|
folderID: string
|
|
) {
|
|
const statsRes = await TachiServerV1Get<{ folder: FolderDocument; stats: UGPTFolderStat }>(
|
|
`/users/${username}/games/${game}/${playtype}/folders/${folderID}/stats`,
|
|
null
|
|
);
|
|
|
|
if (!statsRes.success) {
|
|
throw new Error(`Failed to fetch stats on ${folderID} for ${username}.`);
|
|
}
|
|
|
|
const { folder, stats } = statsRes.body;
|
|
|
|
const gptConfig = GetGamePTConfig(game, playtype);
|
|
|
|
return CreateEmbed()
|
|
.setTitle(`${username}: ${folder.title}`)
|
|
.setURL(
|
|
`${BotConfig.TACHI_SERVER_LOCATION}/dashboard/users/${username}/games/${game}/${playtype}/folders/${folderID}`
|
|
)
|
|
.addField(
|
|
"Lamp Distribution",
|
|
gptConfig.lamps
|
|
.slice()
|
|
.reverse()
|
|
.map((l) => `${l}: ${stats.lamps[l] ?? 0}`)
|
|
.join("\n"),
|
|
true
|
|
)
|
|
.addField(
|
|
"Grade Distribution",
|
|
gptConfig.grades
|
|
.slice()
|
|
.reverse()
|
|
.map((g) => `${g}: ${stats.grades[g] ?? 0}`)
|
|
.join("\n"),
|
|
true
|
|
)
|
|
|
|
.addField("Total Charts", stats.chartCount.toString());
|
|
}
|
|
|
|
export async function CreateFolderTimelineEmbed(
|
|
game: Game,
|
|
playtype: Playtype,
|
|
username: string,
|
|
folderID: string,
|
|
formatMethod: "first" | "recent",
|
|
rawTarget: string
|
|
) {
|
|
const gptConfig = GetGamePTConfig(game, playtype);
|
|
|
|
const { type, value } = ParseTimelineTarget(game, playtype, rawTarget);
|
|
|
|
const prettyValue = gptConfig[type === "grade" ? "grades" : "lamps"][value];
|
|
|
|
const timelineRes = await TachiServerV1Get<UGPTFolderTimeline>(
|
|
`/users/${username}/games/${game}/${playtype}/folders/${folderID}/timeline`,
|
|
null,
|
|
{
|
|
criteriaType: type,
|
|
criteriaValue: value.toString(),
|
|
}
|
|
);
|
|
|
|
if (!timelineRes.success) {
|
|
throw new Error(
|
|
`Failed to retrieve timeline info for ${username} on folderID: ${folderID}.`
|
|
);
|
|
}
|
|
|
|
const { folder, songs, charts, scores } = timelineRes.body;
|
|
|
|
const songMap = CreateSongMap(songs);
|
|
const chartMap = CreateChartMap(charts);
|
|
|
|
let fields: Array<EmbedFieldData> = [];
|
|
|
|
fields = scores
|
|
.filter((e) => e.timeAchieved !== null)
|
|
.sort((a, b) => {
|
|
if (formatMethod === "first") {
|
|
return a.timeAchieved! - b.timeAchieved!;
|
|
}
|
|
|
|
return b.timeAchieved! - a.timeAchieved!;
|
|
})
|
|
.slice(0, 5)
|
|
.map((sc) => ScoreToEmbed(sc, songMap, chartMap));
|
|
|
|
const embed = CreateEmbed()
|
|
.setTitle(
|
|
`${username}: ${folder.title} (${
|
|
formatMethod === "first" ? "First" : "Most Recent"
|
|
} ${prettyValue}s)`
|
|
)
|
|
.setURL(
|
|
`${BotConfig.TACHI_SERVER_LOCATION}/dashboard/users/${username}/games/${game}/${playtype}/folders/${folderID}`
|
|
);
|
|
|
|
if (fields.length === 0) {
|
|
return embed.addField("N/A", "This user has never achieved this target in this folder.");
|
|
}
|
|
|
|
return embed.addFields(fields);
|
|
}
|
|
|
|
export async function CreateSessionEmbed(session: SessionDocument) {
|
|
const { game, playtype } = session;
|
|
|
|
const { charts, scores, songs, user } = await GetSessionInfo(session.sessionID);
|
|
|
|
const embed = CreateEmbed()
|
|
.setTitle(
|
|
`${user.username}: ${session.name}${
|
|
session.timeEnded < ONE_HOUR * 2 ? " (Ongoing!)" : ""
|
|
}`
|
|
)
|
|
.setDescription(session.desc ?? "This session has no description")
|
|
.setURL(
|
|
`${BotConfig.TACHI_SERVER_LOCATION}/dashboard/users/${user.username}/games/${game}/${playtype}/sessions/${session.sessionID}`
|
|
);
|
|
|
|
const songMap = CreateSongMap(songs);
|
|
const chartMap = CreateChartMap(charts);
|
|
|
|
embed
|
|
.addField(
|
|
"Total Lamp Raises",
|
|
session.scoreInfo.filter((e) => !e.isNewScore && e.lampDelta > 0).length.toString(),
|
|
true
|
|
)
|
|
.addField(
|
|
"Total Grade Raises",
|
|
session.scoreInfo.filter((e) => !e.isNewScore && e.gradeDelta > 0).length.toString(),
|
|
true
|
|
);
|
|
|
|
const gptConfig = GetGamePTConfig(game, playtype);
|
|
|
|
embed.addFields(
|
|
scores
|
|
|
|
// don't mutate original array
|
|
.slice(0)
|
|
|
|
// sort on default rating alg
|
|
// @todo Honour the user's preferred rating alg?
|
|
.sort(
|
|
NumericSOV(
|
|
(k) => k.calculatedData[gptConfig.defaultScoreRatingAlg] ?? -Infinity,
|
|
true
|
|
)
|
|
)
|
|
.filter(UniqueOnKey("chartID"))
|
|
|
|
// pick best 5
|
|
.slice(0, 5)
|
|
.map((sc) => ScoreToEmbed(sc, songMap, chartMap))
|
|
);
|
|
|
|
embed.setFooter({
|
|
text: "Note: Discord doesn't give me enough room to express everything about the session properly. Click the blue link to go to the session and see it properly.",
|
|
});
|
|
|
|
return embed;
|
|
}
|
|
|
|
function ScoreToEmbed(
|
|
sc: ScoreDocument,
|
|
songMap: Map<integer, SongDocument>,
|
|
chartMap: Map<string, ChartDocument>
|
|
): EmbedFieldData {
|
|
const { scoreStr, lampStr } = FormatScoreData(sc);
|
|
|
|
return {
|
|
name: Util.escapeMarkdown(
|
|
FormatChart(sc.game, songMap.get(sc.songID)!, chartMap.get(sc.chartID)!)
|
|
),
|
|
value: `${scoreStr}
|
|
${lampStr}
|
|
${FormatDate(sc.timeAchieved!)} (${MillisToSince(sc.timeAchieved!)})`,
|
|
};
|
|
}
|