diff --git a/src/controllers/dynamic/getProfileViewingDataController.ts b/src/controllers/dynamic/getProfileViewingDataController.ts index b02189d0..c1134604 100644 --- a/src/controllers/dynamic/getProfileViewingDataController.ts +++ b/src/controllers/dynamic/getProfileViewingDataController.ts @@ -1,5 +1,5 @@ import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; -import { Guild } from "@/src/models/guildModel"; +import { Guild, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; import { Account } from "@/src/models/loginModel"; @@ -19,99 +19,155 @@ import { } from "@/src/types/inventoryTypes/inventoryTypes"; import { RequestHandler } from "express"; import { catBreadHash } from "../api/inventoryController"; -import { ExportCustoms } from "warframe-public-export-plus"; +import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus"; +import { IStatsClient } from "@/src/types/statTypes"; +import { toStoreItem } from "@/src/services/itemDataService"; export const getProfileViewingDataController: RequestHandler = async (req, res) => { - if (!req.query.playerId) { - res.status(400).end(); - return; - } - const account = await Account.findById(req.query.playerId as string, "DisplayName"); - if (!account) { - res.status(400).send("No account or guild ID specified"); - return; - } - const inventory = (await Inventory.findOne({ accountOwnerId: account._id }))!; - const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; + if (req.query.playerId) { + const account = await Account.findById(req.query.playerId as string, "DisplayName"); + if (!account) { + res.status(409).send("Could not find requested account"); + return; + } + const inventory = (await Inventory.findOne({ accountOwnerId: account._id }))!; - const result: IPlayerProfileViewingDataResult = { - AccountId: toOid(account._id), - DisplayName: account.DisplayName, - PlayerLevel: inventory.PlayerLevel, - LoadOutInventory: { - WeaponSkins: [], - XPInfo: inventory.XPInfo - }, - PlayerSkills: inventory.PlayerSkills, - ChallengeProgress: inventory.ChallengeProgress, - DeathMarks: inventory.DeathMarks, - Harvestable: inventory.Harvestable, - DeathSquadable: inventory.DeathSquadable, - Created: toMongoDate(inventory.Created), - MigratedToConsole: false, - Missions: inventory.Missions, - Affiliations: inventory.Affiliations, - DailyFocus: inventory.DailyFocus, - Wishlist: inventory.Wishlist, - Alignment: inventory.Alignment - }; - if (inventory.CurrentLoadOutIds.length) { - result.LoadOutPreset = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!.toJSON(); - result.LoadOutPreset.ItemId = undefined; - const skins = new Set(); - if (result.LoadOutPreset.s) { - result.LoadOutInventory.Suits = [ - inventory.Suits.id(result.LoadOutPreset.s.ItemId.$oid)!.toJSON() - ]; - resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Suits[0]); + const result: IPlayerProfileViewingDataResult = { + AccountId: toOid(account._id), + DisplayName: account.DisplayName, + PlayerLevel: inventory.PlayerLevel, + LoadOutInventory: { + WeaponSkins: [], + XPInfo: inventory.XPInfo + }, + PlayerSkills: inventory.PlayerSkills, + ChallengeProgress: inventory.ChallengeProgress, + DeathMarks: inventory.DeathMarks, + Harvestable: inventory.Harvestable, + DeathSquadable: inventory.DeathSquadable, + Created: toMongoDate(inventory.Created), + MigratedToConsole: false, + Missions: inventory.Missions, + Affiliations: inventory.Affiliations, + DailyFocus: inventory.DailyFocus, + Wishlist: inventory.Wishlist, + Alignment: inventory.Alignment + }; + await populateLoadout(inventory, result); + if (inventory.GuildId) { + const guild = (await Guild.findById(inventory.GuildId, "Name Tier XP Class Emblem"))!; + populateGuild(guild, result); } - if (result.LoadOutPreset.p) { - result.LoadOutInventory.Pistols = [ - inventory.Pistols.id(result.LoadOutPreset.p.ItemId.$oid)!.toJSON() - ]; - resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Pistols[0]); + for (const key of allDailyAffiliationKeys) { + result[key] = inventory[key]; } - if (result.LoadOutPreset.l) { - result.LoadOutInventory.LongGuns = [ - inventory.LongGuns.id(result.LoadOutPreset.l.ItemId.$oid)!.toJSON() - ]; - resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.LongGuns[0]); - } - if (result.LoadOutPreset.m) { - result.LoadOutInventory.Melee = [ - inventory.Melee.id(result.LoadOutPreset.m.ItemId.$oid)!.toJSON() - ]; - resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Melee[0]); - } - for (const skin of skins) { - result.LoadOutInventory.WeaponSkins.push({ ItemType: skin }); - } - } - if (inventory.GuildId) { - const guild = (await Guild.findById(inventory.GuildId, "Name Tier XP Class"))!; - result.GuildId = toOid(inventory.GuildId); - result.GuildName = guild.Name; - result.GuildTier = guild.Tier; - result.GuildXp = guild.XP; - result.GuildClass = guild.Class; - result.GuildEmblem = false; - } - for (const key of allDailyAffiliationKeys) { - result[key] = inventory[key]; - } - const stats = (await Stats.findOne({ accountOwnerId: account._id }))!.toJSON>(); - delete stats._id; - delete stats.__v; - delete stats.accountOwnerId; + const stats = (await Stats.findOne({ accountOwnerId: account._id }))!.toJSON>(); + delete stats._id; + delete stats.__v; + delete stats.accountOwnerId; - res.json({ - Results: [result], - TechProjects: [], - XpComponents: [], - //XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for - Stats: stats - }); + res.json({ + Results: [result], + TechProjects: [], + XpComponents: [], + //XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for + Stats: stats + }); + } else if (req.query.guildId) { + const guild = await Guild.findById(req.query.guildId, "Name Tier XP Class Emblem TechProjects ClaimedXP"); + if (!guild) { + res.status(409).send("Could not find guild"); + return; + } + const members = await GuildMember.find({ guildId: guild._id, status: 0 }); + const results: IPlayerProfileViewingDataResult[] = []; + for (let i = 0; i != Math.min(4, members.length); ++i) { + const member = members[i]; + const [account, inventory] = await Promise.all([ + Account.findById(member.accountId, "DisplayName"), + Inventory.findOne( + { accountOwnerId: member.accountId }, + "DisplayName PlayerLevel XPInfo LoadOutPresets CurrentLoadOutIds WeaponSkins Suits Pistols LongGuns Melee" + ) + ]); + const result: IPlayerProfileViewingDataResult = { + AccountId: toOid(account!._id), + DisplayName: account!.DisplayName, + PlayerLevel: inventory!.PlayerLevel, + LoadOutInventory: { + WeaponSkins: [], + XPInfo: inventory!.XPInfo + } + }; + await populateLoadout(inventory!, result); + results.push(result); + } + populateGuild(guild, results[0]); + + const combinedStats: IStatsClient = {}; + const statsArr = await Stats.find({ accountOwnerId: { $in: members.map(x => x.accountId) } }).lean(); // need this as POJO so Object.entries works as expected + for (const stats of statsArr) { + for (const [key, value] of Object.entries(stats)) { + if (typeof value == "number" && key != "__v") { + (combinedStats[key as keyof IStatsClient] as number | undefined) ??= 0; + (combinedStats[key as keyof IStatsClient] as number) += value; + } + } + for (const arrayName of ["Weapons", "Enemies", "Scans", "Missions", "PVP"] as const) { + if (stats[arrayName]) { + combinedStats[arrayName] ??= []; + for (const entry of stats[arrayName]) { + const combinedEntry = combinedStats[arrayName].find(x => x.type == entry.type); + if (combinedEntry) { + for (const [key, value] of Object.entries(entry)) { + if (typeof value == "number") { + (combinedEntry[key as keyof typeof combinedEntry] as unknown as + | number + | undefined) ??= 0; + (combinedEntry[key as keyof typeof combinedEntry] as unknown as number) += value; + } + } + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + combinedStats[arrayName].push(entry as any); + } + } + } + } + } + + const xpComponents: IXPComponentClient[] = []; + if (guild.ClaimedXP) { + for (const componentName of guild.ClaimedXP) { + if (componentName.endsWith(".level")) { + const [key] = Object.entries(ExportDojoRecipes.rooms).find( + ([_key, value]) => value.resultType == componentName + )!; + xpComponents.push({ + StoreTypeName: toStoreItem(key) + }); + } else { + const [key] = Object.entries(ExportDojoRecipes.decos).find( + ([_key, value]) => value.resultType == componentName + )!; + xpComponents.push({ + StoreTypeName: toStoreItem(key) + }); + } + } + } + + res.json({ + Results: results, + TechProjects: guild.TechProjects, + XpComponents: xpComponents, + //XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for + Stats: combinedStats + }); + } else { + res.sendStatus(400); + } }; interface IPlayerProfileViewingDataResult extends Partial { @@ -133,20 +189,40 @@ interface IPlayerProfileViewingDataResult extends Partial { GuildXp?: number; GuildClass?: number; GuildEmblem?: boolean; - PlayerSkills: IPlayerSkills; - ChallengeProgress: IChallengeProgress[]; - DeathMarks: string[]; - Harvestable: boolean; - DeathSquadable: boolean; - Created: IMongoDate; - MigratedToConsole: boolean; - Missions: IMission[]; - Affiliations: IAffiliation[]; - DailyFocus: number; - Wishlist: string[]; + PlayerSkills?: IPlayerSkills; + ChallengeProgress?: IChallengeProgress[]; + DeathMarks?: string[]; + Harvestable?: boolean; + DeathSquadable?: boolean; + Created?: IMongoDate; + MigratedToConsole?: boolean; + Missions?: IMission[]; + Affiliations?: IAffiliation[]; + DailyFocus?: number; + Wishlist?: string[]; Alignment?: IAlignment; } +interface IXPComponentClient { + _id?: IOid; + StoreTypeName: string; + TypeName?: string; + PurchaseQuantity?: number; + ProductCategory?: "Recipes"; + Rarity?: "COMMON"; + RegularPrice?: number; + PremiumPrice?: number; + SellingPrice?: number; + DateAddedToManifest?: number; + PrimeSellingPrice?: number; + GuildXp?: number; + ResultPrefab?: string; + ResultDecoration?: string; + ShowInMarket?: boolean; + ShowInInventory?: boolean; + locTags?: Record; +} + let skinLookupTable: Record | undefined; const resolveAndCollectSkins = ( @@ -181,3 +257,51 @@ const resolveAndCollectSkins = ( } } }; + +const populateLoadout = async ( + inventory: TInventoryDatabaseDocument, + result: IPlayerProfileViewingDataResult +): Promise => { + if (inventory.CurrentLoadOutIds.length) { + const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; + result.LoadOutPreset = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!.toJSON(); + result.LoadOutPreset.ItemId = undefined; + const skins = new Set(); + if (result.LoadOutPreset.s) { + result.LoadOutInventory.Suits = [ + inventory.Suits.id(result.LoadOutPreset.s.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Suits[0]); + } + if (result.LoadOutPreset.p) { + result.LoadOutInventory.Pistols = [ + inventory.Pistols.id(result.LoadOutPreset.p.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Pistols[0]); + } + if (result.LoadOutPreset.l) { + result.LoadOutInventory.LongGuns = [ + inventory.LongGuns.id(result.LoadOutPreset.l.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.LongGuns[0]); + } + if (result.LoadOutPreset.m) { + result.LoadOutInventory.Melee = [ + inventory.Melee.id(result.LoadOutPreset.m.ItemId.$oid)!.toJSON() + ]; + resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Melee[0]); + } + for (const skin of skins) { + result.LoadOutInventory.WeaponSkins.push({ ItemType: skin }); + } + } +}; + +const populateGuild = (guild: TGuildDatabaseDocument, result: IPlayerProfileViewingDataResult): void => { + result.GuildId = toOid(guild._id); + result.GuildName = guild.Name; + result.GuildTier = guild.Tier; + result.GuildXp = guild.XP; + result.GuildClass = guild.Class; + result.GuildEmblem = guild.Emblem; +}; diff --git a/src/types/statTypes.ts b/src/types/statTypes.ts index 9907c55d..970d1be5 100644 --- a/src/types/statTypes.ts +++ b/src/types/statTypes.ts @@ -31,6 +31,14 @@ export interface IStatsClient { CaliberChicksScore?: number; OlliesCrashCourseScore?: number; DojoObstacleScore?: number; + + // not in schema + PVP?: { + suitDeaths?: number; + suitKills?: number; + weaponKills?: number; + type: string; + }[]; } export interface IStatsDatabase extends IStatsClient {