diff --git a/app/constants.ts b/app/constants.ts index 8232c5fcb..df9ba17aa 100644 --- a/app/constants.ts +++ b/app/constants.ts @@ -70,6 +70,7 @@ export const NZAP_TEST_DISCORD_ID = "455039198672453645"; export const NZAP_TEST_AVATAR = "f809176af93132c3db5f0a5019e96339"; export const ROOM_PASS_LENGTH = 4; +export const LFG_GROUP_FULL_SIZE = 4; export const TOURNAMENT_TEAM_ROSTER_MIN_SIZE = 4; export const TOURNAMENT_TEAM_ROSTER_MAX_SIZE = 6; /** How many minutes before the start of the tournament check-in closes */ diff --git a/app/models/LFGGroup.server.ts b/app/models/LFGGroup.server.ts index beb316304..192c1c4a5 100644 --- a/app/models/LFGGroup.server.ts +++ b/app/models/LFGGroup.server.ts @@ -38,18 +38,48 @@ export function findActiveByMember(user: { id: string }) { }, }, include: { - members: true - } + members: true, + }, + }); +} + +export function findLookingByType(type: LfgGroupType, ranked: boolean | null) { + return db.lfgGroup.findMany({ + where: { + type, + looking: true, + // For ranked groups we show both ranked and unranked options + ranked: ranked === false ? false : undefined, + }, + select: { + id: true, + ranked: true, + members: { + select: { + user: { + select: { + discordAvatar: true, + discordDiscriminator: true, + discordName: true, + discordId: true, + }, + }, + }, + }, + }, + orderBy: { + createdAt: "desc", + }, }); } export function startLooking(id: string) { return db.lfgGroup.update({ where: { - id + id, }, data: { - looking: true - } - }) + looking: true, + }, + }); } diff --git a/app/routes/play/looking.tsx b/app/routes/play/looking.tsx index ce4983052..e88b696f9 100644 --- a/app/routes/play/looking.tsx +++ b/app/routes/play/looking.tsx @@ -1,3 +1,61 @@ -export default function LookingPage() { - return
Looking...
; +import { json, LoaderFunction, redirect, useLoaderData } from "remix"; +import { LFG_GROUP_FULL_SIZE } from "~/constants"; +import * as LFGGroup from "~/models/LFGGroup.server"; +import { requireUser } from "~/utils"; + +interface LookingLoaderData { + groups: { + id: string; + members?: { + discordId: string; + discordAvatar: string | null; + discordName: string; + discordDiscriminator: string; + }[]; + }[]; +} + +export const loader: LoaderFunction = async ({ context }) => { + const user = requireUser(context); + const ownGroup = await LFGGroup.findActiveByMember(user); + if (!ownGroup) return redirect("/play"); + if (!ownGroup.looking) return redirect("/play/add-players"); + + const groups = await LFGGroup.findLookingByType( + ownGroup.type, + ownGroup.ranked + ); + + // For example if we have a group of 3 and group type is QUAD we only want to consider groups of size 1 + // .. if we have a group size of 2 then we can consider groups of size 1 or 2 and so on + const maxGroupSizeToConsider = (() => { + if (ownGroup.type === "TWIN") return 1; + + return LFG_GROUP_FULL_SIZE - ownGroup.members.length; + })(); + + const lookingForMatch = ownGroup.members.length === LFG_GROUP_FULL_SIZE; + return json({ + groups: groups + .filter((group) => group.members.length <= maxGroupSizeToConsider) + .map((group) => ({ + id: group.id, + // When looking for a match ranked groups are censored + // and instead we only reveal their approximate skill level + members: + group.ranked && lookingForMatch + ? undefined + : group.members.map((member) => member.user), + })), + }); +}; + +export default function LookingPage() { + const data = useLoaderData(); + + return ( +
+
{JSON.stringify(data, null, 2)}
+
+ ); } diff --git a/app/utils/schemas.ts b/app/utils/schemas.ts index c6a72baa4..174a4b9ac 100644 --- a/app/utils/schemas.ts +++ b/app/utils/schemas.ts @@ -13,6 +13,7 @@ export const SeedVariationsSchema = z.enum([ "check-in", "match", "tournament-start", + "looking", ]); export type LoggedInUser = NonNullable< diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2e232954e..66e294818 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -225,6 +225,8 @@ model LfgGroup { likesReceived LfgGroupLike[] @relation("target") @@index([active]) + @@index([looking]) + @@index([type]) } model LfgGroupLike { diff --git a/prisma/seed/script.ts b/prisma/seed/script.ts index ecd6fdc80..e1d4d6467 100644 --- a/prisma/seed/script.ts +++ b/prisma/seed/script.ts @@ -16,6 +16,7 @@ import { createTournamentRounds } from "../../app/services/tournament"; import invariant from "tiny-invariant"; import { SeedVariations } from "~/utils/schemas"; import { v4 as uuidv4 } from "uuid"; +import clone from "just-clone"; const prisma = new PrismaClient(); const mapListDE = `{"losers":[[{"id":4647,"name":"Kelp Dome","mode":"SZ"},{"id":4658,"name":"Blackbelly Skatepark","mode":"TC"},{"id":4645,"name":"Manta Maria","mode":"CB"}],[{"id":4624,"name":"Inkblot Art Academy","mode":"RM"},{"id":4707,"name":"Ancho-V Games","mode":"SZ"},{"id":4618,"name":"Humpback Pump Track","mode":"TC"}],[{"id":4692,"name":"Camp Triggerfish","mode":"SZ"},{"id":4665,"name":"MakoMart","mode":"CB"},{"id":4634,"name":"Moray Towers","mode":"RM"}],[{"id":4609,"name":"Musselforge Fitness","mode":"RM"},{"id":4647,"name":"Kelp Dome","mode":"SZ"},{"id":4678,"name":"Arowana Mall","mode":"TC"}],[{"id":4705,"name":"New Albacore Hotel","mode":"CB"},{"id":4644,"name":"Manta Maria","mode":"RM"},{"id":4707,"name":"Ancho-V Games","mode":"SZ"}],[{"id":4657,"name":"Blackbelly Skatepark","mode":"SZ"},{"id":4690,"name":"Piranha Pit","mode":"CB"},{"id":4682,"name":"Goby Arena","mode":"SZ"},{"id":4678,"name":"Arowana Mall","mode":"TC"},{"id":4624,"name":"Inkblot Art Academy","mode":"RM"}]],"winners":[[{"id":4677,"name":"Arowana Mall","mode":"SZ"},{"id":4665,"name":"MakoMart","mode":"CB"},{"id":4618,"name":"Humpback Pump Track","mode":"TC"}],[{"id":4624,"name":"Inkblot Art Academy","mode":"RM"},{"id":4683,"name":"Goby Arena","mode":"TC"},{"id":4692,"name":"Camp Triggerfish","mode":"SZ"}],[{"id":4647,"name":"Kelp Dome","mode":"SZ"},{"id":4634,"name":"Moray Towers","mode":"RM"},{"id":4707,"name":"Ancho-V Games","mode":"SZ"},{"id":4610,"name":"Musselforge Fitness","mode":"CB"},{"id":4658,"name":"Blackbelly Skatepark","mode":"TC"}],[{"id":4644,"name":"Manta Maria","mode":"RM"},{"id":4677,"name":"Arowana Mall","mode":"SZ"},{"id":4690,"name":"Piranha Pit","mode":"CB"},{"id":4682,"name":"Goby Arena","mode":"SZ"},{"id":4618,"name":"Humpback Pump Track","mode":"TC"}],[{"id":4624,"name":"Inkblot Art Academy","mode":"RM"},{"id":4707,"name":"Ancho-V Games","mode":"SZ"},{"id":4610,"name":"Musselforge Fitness","mode":"CB"},{"id":4657,"name":"Blackbelly Skatepark","mode":"SZ"},{"id":4693,"name":"Camp Triggerfish","mode":"TC"},{"id":4664,"name":"MakoMart","mode":"RM"},{"id":4647,"name":"Kelp Dome","mode":"SZ"}],[{"id":4617,"name":"Humpback Pump Track","mode":"SZ"},{"id":4635,"name":"Moray Towers","mode":"CB"},{"id":4682,"name":"Goby Arena","mode":"SZ"},{"id":4644,"name":"Manta Maria","mode":"RM"},{"id":4678,"name":"Arowana Mall","mode":"TC"},{"id":4692,"name":"Camp Triggerfish","mode":"SZ"},{"id":4705,"name":"New Albacore Hotel","mode":"CB"}]]}`; @@ -45,6 +46,9 @@ export async function seed(variation?: SeedVariations) { await prisma.tournament.deleteMany(); await prisma.organization.deleteMany(); await prisma.trustRelationships.deleteMany(); + await prisma.lfgGroupLike.deleteMany(); + await prisma.lfgGroupMember.deleteMany(); + await prisma.lfgGroup.deleteMany(); await prisma.user.deleteMany(); await prisma.stage.deleteMany(); @@ -61,12 +65,21 @@ export async function seed(variation?: SeedVariations) { await trustRelationship(nzapUserCreated.id, adminUserCreated.id); await stages(); await tournamentAddMaps(tournament.id); + + const userIdsInTheSystem = (await prisma.user.findMany({})).map( + (u) => u.id + ); + await lookingLfgGroups(userIdsInTheSystem); + if (variation === "match" || variation === "tournament-start") { await tournamentRoundsCreate(); } if (variation === "match") { await advanceRound(); } + if (variation === "looking") { + await ownGroup(); + } async function adminUser() { return prisma.user.create({ @@ -357,6 +370,63 @@ export async function seed(variation?: SeedVariations) { return result; } + async function lookingLfgGroups(userIds: string[]) { + function randomIntFromInterval(min: number, max: number) { + // min and max included + return Math.floor(Math.random() * (max - min + 1) + min); + } + + const userIdsStack = clone(userIds); + for (let i = 0; i < 24; i++) { + const amountOfUsers = randomIntFromInterval(1, 3); + await prisma.lfgGroup.create({ + data: { + looking: true, + active: true, + type: "VERSUS", + ranked: i < 12, + members: { + createMany: { + data: new Array(amountOfUsers).fill(null).map((_, i) => ({ + memberId: userIdsStack.shift()!, + captain: i === 0, + })), + }, + }, + }, + }); + } + } + + async function ownGroup() { + const groups = await prisma.lfgGroup.findMany({}); + return prisma.lfgGroup.create({ + data: { + looking: true, + type: "VERSUS", + active: true, + ranked: true, + members: { + createMany: { + data: [ + { + memberId: ADMIN_TEST_UUID, + }, + { memberId: NZAP_TEST_UUID }, + ], + }, + }, + likesReceived: { + createMany: { + data: new Array(10).fill(null).map((_) => ({ + likerId: groups.shift()!.id, + })), + }, + }, + }, + }); + } + async function tournamentRoundsCreate() { const stages = await prisma.stage.findMany({}); await createTournamentRounds({