From 69db988ef5a6c3b78252f2ee4ef8a26608434ff1 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Wed, 11 Oct 2023 19:16:01 +0300 Subject: [PATCH] Show groups closer to own skill tier first --- app/features/sendouq/components/GroupCard.tsx | 2 +- app/features/sendouq/core/groups.server.ts | 111 ++++++++++++++---- app/features/sendouq/q-types.ts | 1 + .../queries/findPreparingGroup.server.ts | 2 + .../sendouq/queries/lookingGroups.server.ts | 3 +- app/features/sendouq/routes/q.looking.tsx | 17 ++- 6 files changed, 110 insertions(+), 26 deletions(-) diff --git a/app/features/sendouq/components/GroupCard.tsx b/app/features/sendouq/components/GroupCard.tsx index 1776a4f51..7a94b432a 100644 --- a/app/features/sendouq/components/GroupCard.tsx +++ b/app/features/sendouq/components/GroupCard.tsx @@ -33,7 +33,7 @@ export function GroupCard({ hideWeapons = false, hideNote: _hidenote = false, }: { - group: LookingGroup; + group: Omit; action?: "LIKE" | "UNLIKE" | "GROUP_UP" | "MATCH_UP"; ownRole?: GroupMemberType["role"]; ownGroup?: boolean; diff --git a/app/features/sendouq/core/groups.server.ts b/app/features/sendouq/core/groups.server.ts index a48375f9b..d03f61d9f 100644 --- a/app/features/sendouq/core/groups.server.ts +++ b/app/features/sendouq/core/groups.server.ts @@ -13,6 +13,7 @@ import type { TieredSkill, } from "~/features/mmr/tiered.server"; import type { RecentMatchPlayer } from "../queries/findRecentMatchPlayersByUserId.server"; +import { TIERS } from "~/features/mmr/mmr-constants"; export function divideGroups({ groups, @@ -165,6 +166,66 @@ export function censorGroups({ }; } +export function sortGroupsBySkill({ + groups, + userSkills, + intervals, +}: { + groups: DividedGroups; + userSkills: Record; + intervals: SkillTierInterval[]; +}): DividedGroups { + const ownGroupTier = + groups.own.tier?.name ?? + resolveGroupSkill({ + group: groups.own as LookingGroupWithInviteCode, + userSkills, + intervals, + })?.name; + const ownGroupTierIndex = TIERS.findIndex((t) => t.name === ownGroupTier); + + const tierDiff = (otherGroupTierName?: string) => { + if (!otherGroupTierName) return 10; + + const otherGroupTierIndex = TIERS.findIndex( + (t) => t.name === otherGroupTierName, + ); + + return Math.abs(ownGroupTierIndex - otherGroupTierIndex); + }; + + return { + ...groups, + neutral: groups.neutral.sort((a, b) => { + const aTier = + a.tier?.name ?? + resolveGroupSkill({ + group: a as LookingGroupWithInviteCode, + userSkills, + intervals, + })?.name; + const bTier = + b.tier?.name ?? + resolveGroupSkill({ + group: b as LookingGroupWithInviteCode, + userSkills, + intervals, + })?.name; + + const aTierDiff = tierDiff(aTier); + const bTierDiff = tierDiff(bTier); + + // if same tier difference, show newer groups first + if (aTierDiff === bTierDiff) { + return b.createdAt - a.createdAt; + } + + // show groups with smaller tier difference first + return aTierDiff - bTierDiff; + }), + }; +} + export function addSkillsToGroups({ groups, userSkills, @@ -174,33 +235,16 @@ export function addSkillsToGroups({ userSkills: Record; intervals: SkillTierInterval[]; }): DividedGroupsUncensored { - const resolveGroupSkill = ( - group: LookingGroupWithInviteCode, - ): TieredSkill["tier"] | undefined => { - if (group.members.length < FULL_GROUP_SIZE) return; - - const skills = group.members - .map((m) => userSkills[String(m.id)]) - .filter(Boolean); - const averageOrdinal = - skills.reduce((acc, s) => acc + s.ordinal, 0) / skills.length; - - const tier = intervals.find( - (i) => i.neededOrdinal && averageOrdinal > i.neededOrdinal, - ) ?? { isPlus: false, name: "IRON" }; - - // For Leviathan we don't specify if it's plus or not - return tier.name === "LEVIATHAN" - ? { name: "LEVIATHAN", isPlus: false } - : { name: tier.name, isPlus: tier.isPlus }; - }; const addSkill = (group: LookingGroupWithInviteCode) => ({ ...group, members: group.members?.map((m) => ({ ...m, skill: userSkills[String(m.id)], })), - tier: resolveGroupSkill(group), + tier: + group.members.length === FULL_GROUP_SIZE + ? resolveGroupSkill({ group, userSkills, intervals }) + : undefined, }); return { @@ -214,6 +258,31 @@ export function membersNeededForFull(currentSize: number) { return FULL_GROUP_SIZE - currentSize; } +function resolveGroupSkill({ + group, + userSkills, + intervals, +}: { + group: LookingGroupWithInviteCode; + userSkills: Record; + intervals: SkillTierInterval[]; +}): TieredSkill["tier"] | undefined { + const skills = group.members + .map((m) => userSkills[String(m.id)]) + .filter(Boolean); + const averageOrdinal = + skills.reduce((acc, s) => acc + s.ordinal, 0) / skills.length; + + const tier = intervals.find( + (i) => i.neededOrdinal && averageOrdinal > i.neededOrdinal, + ) ?? { isPlus: false, name: "IRON" }; + + // For Leviathan we don't specify if it's plus or not + return tier.name === "LEVIATHAN" + ? { name: "LEVIATHAN", isPlus: false } + : { name: tier.name, isPlus: tier.isPlus }; +} + export function groupExpiryStatus( group: Pick, ): null | "EXPIRING_SOON" | "EXPIRED" { diff --git a/app/features/sendouq/q-types.ts b/app/features/sendouq/q-types.ts index ebaefa424..b9830dd98 100644 --- a/app/features/sendouq/q-types.ts +++ b/app/features/sendouq/q-types.ts @@ -12,6 +12,7 @@ import type { GroupForMatch } from "./queries/groupForMatch.server"; export type LookingGroup = { id: number; mapListPreference?: Group["mapListPreference"]; + createdAt: Group["createdAt"]; tier?: TieredSkill["tier"]; isReplay?: boolean; isLiked?: boolean; diff --git a/app/features/sendouq/queries/findPreparingGroup.server.ts b/app/features/sendouq/queries/findPreparingGroup.server.ts index c08161995..ac3b75014 100644 --- a/app/features/sendouq/queries/findPreparingGroup.server.ts +++ b/app/features/sendouq/queries/findPreparingGroup.server.ts @@ -34,6 +34,7 @@ const stm = sql.prepare(/* sql */ ` "q1"."id", "q1"."mapListPreference", "q1"."inviteCode", + "q1"."createdAt", json_group_array( json_object( 'id', "q1"."userId", @@ -57,6 +58,7 @@ export function findPreparingGroup( return { id: row.id, + createdAt: row.createdAt, mapListPreference: row.mapListPreference, inviteCode: row.inviteCode, members: parseDBJsonArray(row.members).map((member: any) => { diff --git a/app/features/sendouq/queries/lookingGroups.server.ts b/app/features/sendouq/queries/lookingGroups.server.ts index 81459e979..9d597f17b 100644 --- a/app/features/sendouq/queries/lookingGroups.server.ts +++ b/app/features/sendouq/queries/lookingGroups.server.ts @@ -46,6 +46,7 @@ const stm = sql.prepare(/* sql */ ` "q1"."id", "q1"."mapListPreference", "q1"."inviteCode", + "q1"."createdAt", json_group_array( json_object( 'id', "q1"."userId", @@ -64,7 +65,6 @@ const stm = sql.prepare(/* sql */ ` ) as "members" from "q1" group by "q1"."id" - order by "q1"."createdAt" desc `); export function findLookingGroups({ @@ -83,6 +83,7 @@ export function findLookingGroups({ id: row.id, mapListPreference: row.mapListPreference, inviteCode: row.inviteCode, + createdAt: row.createdAt, members: parseDBJsonArray(row.members).map((member: any) => { const weapons = parseDBArray(member.weapons); diff --git a/app/features/sendouq/routes/q.looking.tsx b/app/features/sendouq/routes/q.looking.tsx index ed4d5965b..258cea752 100644 --- a/app/features/sendouq/routes/q.looking.tsx +++ b/app/features/sendouq/routes/q.looking.tsx @@ -37,6 +37,7 @@ import { filterOutGroupsWithIncompatibleMapListPreference, groupExpiryStatus, membersNeededForFull, + sortGroupsBySkill, } from "../core/groups.server"; import { createMatchMemento, matchMapList } from "../core/match.server"; import { FULL_GROUP_SIZE } from "../q-constants"; @@ -324,9 +325,13 @@ export const loader = async ({ request }: LoaderArgs) => { const season = currentOrPreviousSeason(new Date()); + const { intervals, userSkills: calculatedUserSkills } = await userSkills( + season!.nth, + ); const groupsWithSkills = addSkillsToGroups({ groups: dividedGroups, - ...(await userSkills(season!.nth)), + intervals, + userSkills: calculatedUserSkills, }); const compatibleGroups = groupIsFull @@ -347,12 +352,18 @@ export const loader = async ({ request }: LoaderArgs) => { showInviteCode: hasGroupManagerPerms(currentGroup.role) && !groupIsFull, }); - return { + const sortedGroups = sortGroupsBySkill({ groups: censoredGroups, + intervals, + userSkills: calculatedUserSkills, + }); + + return { + groups: sortedGroups, role: currentGroup.role, chatCode: // don't chat with yourself... - censoredGroups.own.members!.length > 1 ? currentGroup.chatCode : null, + sortedGroups.own.members!.length > 1 ? currentGroup.chatCode : null, lastUpdated: new Date().getTime(), expiryStatus: groupExpiryStatus(currentGroup), trustedPlayers: hasGroupManagerPerms(currentGroup.role)