From f00cd26e0e024aef0510dbfa7336b5d126a3e930 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:00:05 +0200 Subject: [PATCH] Progress incl. create random team names, db rework --- app/db/seed/index.ts | 236 +- app/db/tables.ts | 34 +- .../TournamentLFGRepository.server.test.ts | 362 ++- .../TournamentLFGRepository.server.ts | 349 ++- .../actions/to.$id.looking.server.ts | 77 +- .../components/LFGGroupCard.tsx | 26 +- .../loaders/to.$id.looking.server.ts | 22 +- .../tournament-lfg/routes/to.$id.looking.tsx | 14 +- .../tournament-lfg-schemas.server.ts | 6 +- .../tournament-lfg/tournament-lfg-utils.ts | 10 +- .../tournament/TournamentRepository.server.ts | 2 + .../tournament/routes/to.$id.register.tsx | 2 + app/utils/team-name-data.ts | 2037 +++++++++++++++++ app/utils/team-name.ts | 12 + db-test.sqlite3 | Bin 1179648 -> 1146880 bytes docs/dev/database-relations.md | 2 + migrations/119-tournament-lfg.js | 66 +- 17 files changed, 2558 insertions(+), 699 deletions(-) create mode 100644 app/utils/team-name-data.ts create mode 100644 app/utils/team-name.ts diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts index 94e926ef1..3e10306a3 100644 --- a/app/db/seed/index.ts +++ b/app/db/seed/index.ts @@ -40,6 +40,7 @@ import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.s import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps"; import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server"; import { AMOUNT_OF_MAPS_IN_POOL_PER_MODE } from "~/features/sendouq-settings/q-settings-constants"; +import { joinTeam } from "~/features/tournament/queries/joinLeaveTeam.server"; import { TOURNAMENT } from "~/features/tournament/tournament-constants"; import { clearAllTournamentDataCache } from "~/features/tournament-bracket/core/Tournament.server"; import * as TournamentLFGRepository from "~/features/tournament-lfg/TournamentLFGRepository.server"; @@ -283,8 +284,6 @@ function wipeDB() { "TournamentMatchGameResult", "TournamentTeamCheckIn", "TournamentLFGLike", - "TournamentLFGGroupMember", - "TournamentLFGGroup", "TournamentTeam", "TournamentStage", "TournamentResult", @@ -1647,168 +1646,115 @@ async function tournamentLfgGroups() { { tournamentId: 3, teamId: 201 }, ]; - const insertMember = sql.prepare( - /* sql */ `INSERT INTO "TournamentLFGGroupMember" ("groupId", "tournamentId", "userId", "role") VALUES (@groupId, @tournamentId, @userId, @role)`, - ); - const updateNote = sql.prepare( - /* sql */ `UPDATE "TournamentLFGGroupMember" SET "note" = @note WHERE "groupId" = @groupId AND "userId" = @userId`, - ); - const insertLike = sql.prepare( - /* sql */ `INSERT INTO "TournamentLFGLike" ("likerGroupId", "targetGroupId") VALUES (@likerGroupId, @targetGroupId)`, - ); - const insertTeam = sql.prepare( - /* sql */ `INSERT INTO "TournamentTeam" ("name", "createdAt", "tournamentId", "inviteCode") VALUES (@name, @createdAt, @tournamentId, @inviteCode)`, - ); - const insertTeamMember = sql.prepare( - /* sql */ `INSERT INTO "TournamentTeamMember" ("tournamentTeamId", "userId", "isOwner", "createdAt") VALUES (@tournamentTeamId, @userId, @isOwner, @createdAt)`, - ); - const updateGroupTeamId = sql.prepare( - /* sql */ `UPDATE "TournamentLFGGroup" SET "tournamentTeamId" = @teamId WHERE "id" = @groupId`, - ); - - const createTeamForGroup = ( - groupId: number, - tournamentId: number, - ownerUserId: number, - memberUserIds: number[], - ) => { - const now = dateToDatabaseTimestamp(new Date()); - const { lastInsertRowid } = insertTeam.run({ - name: validTournamentTeamName(), - createdAt: now, - tournamentId, - inviteCode: shortNanoid(), - }); - const teamId = Number(lastInsertRowid); - - insertTeamMember.run({ - tournamentTeamId: teamId, - userId: ownerUserId, - isOwner: 1, - createdAt: now, - }); - for (const userId of memberUserIds) { - insertTeamMember.run({ - tournamentTeamId: teamId, - userId, - isOwner: 0, - createdAt: now, - }); - } - - updateGroupTeamId.run({ teamId, groupId }); - }; - let userIndex = 0; for (const { tournamentId, teamId } of tournaments) { - const teamOwner = ( - sql - .prepare( - /* sql */ `SELECT "userId" FROM "TournamentTeamMember" WHERE "tournamentTeamId" = @teamId AND "isOwner" = 1`, - ) - .get({ teamId }) as { userId: number } - ).userId; - const users = availableUsers.slice(userIndex, userIndex + 12); userIndex += 12; - // Group 1: solo, has note, isStayAsSub=1 - const group1 = await TournamentLFGRepository.createGroup({ - tournamentId, - userId: users[0], - isStayAsSub: true, - }); - updateNote.run({ - note: "Looking for a team, can play any role", - groupId: group1.id, - userId: users[0], - }); - - // Group 2: duo, one member has note - const group2 = await TournamentLFGRepository.createGroup({ - tournamentId, - userId: users[1], - }); - insertMember.run({ - groupId: group2.id, - tournamentId, - userId: users[2], - role: "REGULAR", - }); - updateNote.run({ - note: "Prefer backline weapons", - groupId: group2.id, - userId: users[2], - }); - createTeamForGroup(group2.id, tournamentId, users[1], [users[2]]); - - // Group 3: trio - const group3 = await TournamentLFGRepository.createGroup({ - tournamentId, - userId: users[3], - }); - for (const userId of [users[4], users[5]]) { - insertMember.run({ - groupId: group3.id, + // Group 1: solo placeholder, has note, isStayAsSub=1 + const { id: team1Id } = await TournamentLFGRepository.createPlaceholderTeam( + { tournamentId, - userId, - role: "REGULAR", - }); - } - createTeamForGroup(group3.id, tournamentId, users[3], [users[4], users[5]]); - - // Group 4: full (4 members) - const group4 = await TournamentLFGRepository.createGroup({ - tournamentId, - userId: users[6], + userId: users[0], + isStayAsSub: true, + }, + ); + await TournamentLFGRepository.updateTeamNote({ + teamId: team1Id, + value: "Looking for a team, can play any role", }); - for (const userId of [users[7], users[8], users[9]]) { - insertMember.run({ - groupId: group4.id, + + // Group 2: duo placeholder + const { id: team2Id } = await TournamentLFGRepository.createPlaceholderTeam( + { tournamentId, - userId, - role: "REGULAR", - }); - } - createTeamForGroup(group4.id, tournamentId, users[6], [ - users[7], - users[8], - users[9], - ]); - - // Group 5: solo - const group5 = await TournamentLFGRepository.createGroup({ + userId: users[1], + }, + ); + joinTeam({ + newTeamId: team2Id, + userId: users[2], + inGameName: null, tournamentId, - userId: users[10], }); - // Group 6: duo linked to existing team - const group6 = await TournamentLFGRepository.createGroup({ + // Group 3: trio placeholder + const { id: team3Id } = await TournamentLFGRepository.createPlaceholderTeam( + { + tournamentId, + userId: users[3], + }, + ); + joinTeam({ + newTeamId: team3Id, + userId: users[4], + inGameName: null, tournamentId, - userId: teamOwner, }); - updateGroupTeamId.run({ teamId, groupId: group6.id }); - insertMember.run({ - groupId: group6.id, + joinTeam({ + newTeamId: team3Id, + userId: users[5], + inGameName: null, tournamentId, + }); + + // Group 4: full (4 members) placeholder + const { id: team4Id } = await TournamentLFGRepository.createPlaceholderTeam( + { + tournamentId, + userId: users[6], + }, + ); + joinTeam({ + newTeamId: team4Id, + userId: users[7], + inGameName: null, + tournamentId, + }); + joinTeam({ + newTeamId: team4Id, + userId: users[8], + inGameName: null, + tournamentId, + }); + joinTeam({ + newTeamId: team4Id, + userId: users[9], + inGameName: null, + tournamentId, + }); + + // Group 5: solo placeholder + const { id: team5Id } = await TournamentLFGRepository.createPlaceholderTeam( + { + tournamentId, + userId: users[10], + }, + ); + + // Group 6: existing team set to looking + await TournamentLFGRepository.startLooking(teamId); + joinTeam({ + newTeamId: teamId, userId: users[11], - role: "REGULAR", + inGameName: null, + tournamentId, }); - // Group 1 -> Group 2 (one-way like) - insertLike.run({ - likerGroupId: group1.id, - targetGroupId: group2.id, + // Team 1 -> Team 2 (one-way like) + await TournamentLFGRepository.addLike({ + likerTeamId: team1Id, + targetTeamId: team2Id, }); - // Group 2 -> Group 1 (mutual — tests invitation UI) - insertLike.run({ - likerGroupId: group2.id, - targetGroupId: group1.id, + // Team 2 -> Team 1 (mutual — tests invitation UI) + await TournamentLFGRepository.addLike({ + likerTeamId: team2Id, + targetTeamId: team1Id, }); - // Group 3 -> Group 5 (one-way like) - insertLike.run({ - likerGroupId: group3.id, - targetGroupId: group5.id, + // Team 3 -> Team 5 (one-way like) + await TournamentLFGRepository.addLike({ + likerTeamId: team3Id, + targetTeamId: team5Id, }); } } diff --git a/app/db/tables.ts b/app/db/tables.ts index 279abb7d7..e31e5c494 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -693,29 +693,9 @@ export interface TournamentSub { visibility: "+1" | "+2" | "+3" | "ALL"; } -export interface TournamentLFGGroup { - id: GeneratedAlways; - tournamentId: number; - tournamentTeamId: number | null; - visibility: JSONColumnTypeNullable; - chatCode: string; - createdAt: Generated; -} - -// xxx: duplicate state with tournament team member... keeping these up to date might be annoying -export interface TournamentLFGGroupMember { - groupId: number; - tournamentId: number; - userId: number; - role: "OWNER" | "MANAGER" | "REGULAR"; - note: string | null; - isStayAsSub: Generated; - createdAt: Generated; -} - export interface TournamentLFGLike { - likerGroupId: number; - targetGroupId: number; + likerTeamId: number; + targetTeamId: number; createdAt: Generated; } @@ -739,6 +719,12 @@ export interface TournamentTeam { tournamentId: number; teamId: number | null; avatarImgId: number | null; + isLooking: Generated; + isPlaceholder: Generated; + lfgVisibility: JSONColumnTypeNullable; + lfgNote: string | null; + // xxx: do we provide chat at this point? if not then remove + chatCode: string | null; } export interface TournamentTeamCheckIn { @@ -756,6 +742,8 @@ export interface TournamentTeamMember { inGameName: string | null; tournamentTeamId: number; userId: number; + role: Generated<"OWNER" | "MANAGER" | "REGULAR">; + isStayAsSub: Generated; } export interface TournamentOrganization { @@ -1232,8 +1220,6 @@ export interface DB { Tournament: Tournament; TournamentStaff: TournamentStaff; TournamentGroup: TournamentGroup; - TournamentLFGGroup: TournamentLFGGroup; - TournamentLFGGroupMember: TournamentLFGGroupMember; TournamentLFGLike: TournamentLFGLike; TournamentMatch: TournamentMatch; TournamentMatchPickBanEvent: TournamentMatchPickBanEvent; diff --git a/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts b/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts index 4497622f5..300d42a5b 100644 --- a/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts +++ b/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts @@ -13,10 +13,10 @@ const createTournament = () => .returning("id") .executeTakeFirstOrThrow(); -const createGroup = (tournamentId: number, userId: number) => - TournamentLFGRepository.createGroup({ tournamentId, userId }); +const createPlaceholder = (tournamentId: number, userId: number) => + TournamentLFGRepository.createPlaceholderTeam({ tournamentId, userId }); -describe("createGroup", () => { +describe("createPlaceholderTeam", () => { beforeEach(async () => { await dbInsertUsers(2); }); @@ -25,23 +25,23 @@ describe("createGroup", () => { dbReset(); }); - test("creates a group with owner member", async () => { + test("creates a placeholder team with owner member", async () => { const tournament = await createTournament(); - const group = await createGroup(tournament.id, 1); + const team = await createPlaceholder(tournament.id, 1); - const groups = await TournamentLFGRepository.findGroupsByTournamentId( + const groups = await TournamentLFGRepository.findLookingTeamsByTournamentId( tournament.id, ); expect(groups).toHaveLength(1); - expect(groups[0].id).toBe(group.id); + expect(groups[0].id).toBe(team.id); }); test("owner has OWNER role", async () => { const tournament = await createTournament(); - await createGroup(tournament.id, 1); + await createPlaceholder(tournament.id, 1); - const groups = await TournamentLFGRepository.findGroupsByTournamentId( + const groups = await TournamentLFGRepository.findLookingTeamsByTournamentId( tournament.id, ); const members = groups[0].members; @@ -50,7 +50,7 @@ describe("createGroup", () => { }); }); -describe("findGroupsByTournamentId", () => { +describe("findLookingTeamsByTournamentId", () => { beforeEach(async () => { await dbInsertUsers(3); }); @@ -59,22 +59,22 @@ describe("findGroupsByTournamentId", () => { dbReset(); }); - test("returns groups for given tournament", async () => { + test("returns looking teams for given tournament", async () => { const tournament = await createTournament(); - await createGroup(tournament.id, 1); - await createGroup(tournament.id, 2); + await createPlaceholder(tournament.id, 1); + await createPlaceholder(tournament.id, 2); - const groups = await TournamentLFGRepository.findGroupsByTournamentId( + const groups = await TournamentLFGRepository.findLookingTeamsByTournamentId( tournament.id, ); expect(groups).toHaveLength(2); }); - test("returns empty array when no groups exist", async () => { + test("returns empty array when no looking teams exist", async () => { const tournament = await createTournament(); - const groups = await TournamentLFGRepository.findGroupsByTournamentId( + const groups = await TournamentLFGRepository.findLookingTeamsByTournamentId( tournament.id, ); @@ -83,9 +83,9 @@ describe("findGroupsByTournamentId", () => { test("returns groups with member data", async () => { const tournament = await createTournament(); - await createGroup(tournament.id, 1); + await createPlaceholder(tournament.id, 1); - const groups = await TournamentLFGRepository.findGroupsByTournamentId( + const groups = await TournamentLFGRepository.findLookingTeamsByTournamentId( tournament.id, ); const members = groups[0].members; @@ -95,13 +95,13 @@ describe("findGroupsByTournamentId", () => { expect(members[0].role).toBe("OWNER"); }); - test("does not return groups from other tournaments", async () => { + test("does not return teams from other tournaments", async () => { const tournament1 = await createTournament(); const tournament2 = await createTournament(); - await createGroup(tournament1.id, 1); - await createGroup(tournament2.id, 2); + await createPlaceholder(tournament1.id, 1); + await createPlaceholder(tournament2.id, 2); - const groups = await TournamentLFGRepository.findGroupsByTournamentId( + const groups = await TournamentLFGRepository.findLookingTeamsByTournamentId( tournament1.id, ); @@ -118,62 +118,62 @@ describe("addLike / deleteLike", () => { dbReset(); }); - test("adds a like between two groups", async () => { + test("adds a like between two teams", async () => { const tournament = await createTournament(); - const group1 = await createGroup(tournament.id, 1); - const group2 = await createGroup(tournament.id, 2); + const team1 = await createPlaceholder(tournament.id, 1); + const team2 = await createPlaceholder(tournament.id, 2); await TournamentLFGRepository.addLike({ - likerGroupId: group1.id, - targetGroupId: group2.id, + likerTeamId: team1.id, + targetTeamId: team2.id, }); - const likes = await TournamentLFGRepository.allLikesByGroupId(group1.id); + const likes = await TournamentLFGRepository.allLikesByTeamId(team1.id); expect(likes.given).toHaveLength(1); - expect(likes.given[0].groupId).toBe(group2.id); + expect(likes.given[0].teamId).toBe(team2.id); }); test("duplicate like does not throw", async () => { const tournament = await createTournament(); - const group1 = await createGroup(tournament.id, 1); - const group2 = await createGroup(tournament.id, 2); + const team1 = await createPlaceholder(tournament.id, 1); + const team2 = await createPlaceholder(tournament.id, 2); await TournamentLFGRepository.addLike({ - likerGroupId: group1.id, - targetGroupId: group2.id, + likerTeamId: team1.id, + targetTeamId: team2.id, }); await TournamentLFGRepository.addLike({ - likerGroupId: group1.id, - targetGroupId: group2.id, + likerTeamId: team1.id, + targetTeamId: team2.id, }); - const likes = await TournamentLFGRepository.allLikesByGroupId(group1.id); + const likes = await TournamentLFGRepository.allLikesByTeamId(team1.id); expect(likes.given).toHaveLength(1); }); test("deleteLike removes the like", async () => { const tournament = await createTournament(); - const group1 = await createGroup(tournament.id, 1); - const group2 = await createGroup(tournament.id, 2); + const team1 = await createPlaceholder(tournament.id, 1); + const team2 = await createPlaceholder(tournament.id, 2); await TournamentLFGRepository.addLike({ - likerGroupId: group1.id, - targetGroupId: group2.id, + likerTeamId: team1.id, + targetTeamId: team2.id, }); await TournamentLFGRepository.deleteLike({ - likerGroupId: group1.id, - targetGroupId: group2.id, + likerTeamId: team1.id, + targetTeamId: team2.id, }); - const likes = await TournamentLFGRepository.allLikesByGroupId(group1.id); + const likes = await TournamentLFGRepository.allLikesByTeamId(team1.id); expect(likes.given).toHaveLength(0); }); }); -describe("allLikesByGroupId", () => { +describe("allLikesByTeamId", () => { beforeEach(async () => { await dbInsertUsers(3); }); @@ -184,39 +184,39 @@ describe("allLikesByGroupId", () => { test("separates likes into given and received", async () => { const tournament = await createTournament(); - const group1 = await createGroup(tournament.id, 1); - const group2 = await createGroup(tournament.id, 2); - const group3 = await createGroup(tournament.id, 3); + const team1 = await createPlaceholder(tournament.id, 1); + const team2 = await createPlaceholder(tournament.id, 2); + const team3 = await createPlaceholder(tournament.id, 3); await TournamentLFGRepository.addLike({ - likerGroupId: group1.id, - targetGroupId: group2.id, + likerTeamId: team1.id, + targetTeamId: team2.id, }); await TournamentLFGRepository.addLike({ - likerGroupId: group3.id, - targetGroupId: group1.id, + likerTeamId: team3.id, + targetTeamId: team1.id, }); - const likes = await TournamentLFGRepository.allLikesByGroupId(group1.id); + const likes = await TournamentLFGRepository.allLikesByTeamId(team1.id); expect(likes.given).toHaveLength(1); - expect(likes.given[0].groupId).toBe(group2.id); + expect(likes.given[0].teamId).toBe(team2.id); expect(likes.received).toHaveLength(1); - expect(likes.received[0].groupId).toBe(group3.id); + expect(likes.received[0].teamId).toBe(team3.id); }); test("returns empty given/received when no likes", async () => { const tournament = await createTournament(); - const group = await createGroup(tournament.id, 1); + const team = await createPlaceholder(tournament.id, 1); - const likes = await TournamentLFGRepository.allLikesByGroupId(group.id); + const likes = await TournamentLFGRepository.allLikesByTeamId(team.id); expect(likes.given).toHaveLength(0); expect(likes.received).toHaveLength(0); }); }); -describe("morphGroups", () => { +describe("mergeTeams", () => { beforeEach(async () => { await dbInsertUsers(5); }); @@ -225,41 +225,39 @@ describe("morphGroups", () => { dbReset(); }); - test("merges two groups, other group is deleted", async () => { + test("merges two teams, other team is deleted", async () => { const tournament = await createTournament(); - const group1 = await createGroup(tournament.id, 1); - const group2 = await createGroup(tournament.id, 2); + const team1 = await createPlaceholder(tournament.id, 1); + const team2 = await createPlaceholder(tournament.id, 2); - await TournamentLFGRepository.morphGroups({ - survivingGroupId: group1.id, - otherGroupId: group2.id, + await TournamentLFGRepository.mergeTeams({ + survivingTeamId: team1.id, + otherTeamId: team2.id, maxGroupSize: 4, - tournamentId: tournament.id, }); - const groups = await TournamentLFGRepository.findGroupsByTournamentId( + const groups = await TournamentLFGRepository.findLookingTeamsByTournamentId( tournament.id, ); expect(groups).toHaveLength(1); - expect(groups[0].id).toBe(group1.id); + expect(groups[0].id).toBe(team1.id); const members = groups[0].members; expect(members).toHaveLength(2); }); - test("demotes other group's OWNER to MANAGER", async () => { + test("demotes other team's OWNER to MANAGER", async () => { const tournament = await createTournament(); - const group1 = await createGroup(tournament.id, 1); - const group2 = await createGroup(tournament.id, 2); + const team1 = await createPlaceholder(tournament.id, 1); + const team2 = await createPlaceholder(tournament.id, 2); - await TournamentLFGRepository.morphGroups({ - survivingGroupId: group1.id, - otherGroupId: group2.id, + await TournamentLFGRepository.mergeTeams({ + survivingTeamId: team1.id, + otherTeamId: team2.id, maxGroupSize: 4, - tournamentId: tournament.id, }); - const groups = await TournamentLFGRepository.findGroupsByTournamentId( + const groups = await TournamentLFGRepository.findLookingTeamsByTournamentId( tournament.id, ); const members = groups[0].members; @@ -270,49 +268,47 @@ describe("morphGroups", () => { test("throws when merged size exceeds maxGroupSize", async () => { const tournament = await createTournament(); - const group1 = await createGroup(tournament.id, 1); - const group2 = await createGroup(tournament.id, 2); + const team1 = await createPlaceholder(tournament.id, 1); + const team2 = await createPlaceholder(tournament.id, 2); await expect( - TournamentLFGRepository.morphGroups({ - survivingGroupId: group1.id, - otherGroupId: group2.id, + TournamentLFGRepository.mergeTeams({ + survivingTeamId: team1.id, + otherTeamId: team2.id, maxGroupSize: 1, - tournamentId: tournament.id, }), ).rejects.toThrow("Group has too many members after merge"); }); - test("clears likes on surviving group after merge", async () => { + test("clears likes on surviving team after merge", async () => { const tournament = await createTournament(); - const group1 = await createGroup(tournament.id, 1); - const group2 = await createGroup(tournament.id, 2); - const group3 = await createGroup(tournament.id, 3); + const team1 = await createPlaceholder(tournament.id, 1); + const team2 = await createPlaceholder(tournament.id, 2); + const team3 = await createPlaceholder(tournament.id, 3); await TournamentLFGRepository.addLike({ - likerGroupId: group1.id, - targetGroupId: group3.id, + likerTeamId: team1.id, + targetTeamId: team3.id, }); await TournamentLFGRepository.addLike({ - likerGroupId: group3.id, - targetGroupId: group1.id, + likerTeamId: team3.id, + targetTeamId: team1.id, }); - await TournamentLFGRepository.morphGroups({ - survivingGroupId: group1.id, - otherGroupId: group2.id, + await TournamentLFGRepository.mergeTeams({ + survivingTeamId: team1.id, + otherTeamId: team2.id, maxGroupSize: 4, - tournamentId: tournament.id, }); - const likes = await TournamentLFGRepository.allLikesByGroupId(group1.id); + const likes = await TournamentLFGRepository.allLikesByTeamId(team1.id); expect(likes.given).toHaveLength(0); expect(likes.received).toHaveLength(0); }); }); -describe("updateMemberNote", () => { +describe("updateTeamNote", () => { beforeEach(async () => { await dbInsertUsers(1); }); @@ -321,33 +317,29 @@ describe("updateMemberNote", () => { dbReset(); }); - test("sets and clears a member note", async () => { + test("sets and clears a team note", async () => { const tournament = await createTournament(); - const group = await createGroup(tournament.id, 1); + const team = await createPlaceholder(tournament.id, 1); - await TournamentLFGRepository.updateMemberNote({ - groupId: group.id, - userId: 1, + await TournamentLFGRepository.updateTeamNote({ + teamId: team.id, value: "Looking for support", }); - let groups = await TournamentLFGRepository.findGroupsByTournamentId( + let groups = await TournamentLFGRepository.findLookingTeamsByTournamentId( tournament.id, ); - let members = groups[0].members; - expect(members[0].note).toBe("Looking for support"); + expect(groups[0].note).toBe("Looking for support"); - await TournamentLFGRepository.updateMemberNote({ - groupId: group.id, - userId: 1, + await TournamentLFGRepository.updateTeamNote({ + teamId: team.id, value: null, }); - groups = await TournamentLFGRepository.findGroupsByTournamentId( + groups = await TournamentLFGRepository.findLookingTeamsByTournamentId( tournament.id, ); - members = groups[0].members; - expect(members[0].note).toBeNull(); + expect(groups[0].note).toBeNull(); }); }); @@ -362,25 +354,25 @@ describe("updateMemberRole", () => { test("changes role from REGULAR to MANAGER", async () => { const tournament = await createTournament(); - const group = await createGroup(tournament.id, 1); + const team = await createPlaceholder(tournament.id, 1); await db - .insertInto("TournamentLFGGroupMember") + .insertInto("TournamentTeamMember") .values({ - groupId: group.id, - tournamentId: tournament.id, + tournamentTeamId: team.id, userId: 2, + isOwner: 0, role: "REGULAR", }) .execute(); await TournamentLFGRepository.updateMemberRole({ userId: 2, - groupId: group.id, + teamId: team.id, role: "MANAGER", }); - const groups = await TournamentLFGRepository.findGroupsByTournamentId( + const groups = await TournamentLFGRepository.findLookingTeamsByTournamentId( tournament.id, ); const members = groups[0].members; @@ -393,7 +385,7 @@ describe("updateMemberRole", () => { expect(() => TournamentLFGRepository.updateMemberRole({ userId: 1, - groupId: 1, + teamId: 1, role: "OWNER", }), ).toThrow("Can't set role to OWNER with this function"); @@ -411,10 +403,10 @@ describe("updateStayAsSub", () => { test("toggles isStayAsSub on/off", async () => { const tournament = await createTournament(); - const group = await createGroup(tournament.id, 1); + const team = await createPlaceholder(tournament.id, 1); await TournamentLFGRepository.updateStayAsSub({ - groupId: group.id, + teamId: team.id, userId: 1, value: true, }); @@ -425,7 +417,7 @@ describe("updateStayAsSub", () => { expect(subs).toContain(1); await TournamentLFGRepository.updateStayAsSub({ - groupId: group.id, + teamId: team.id, userId: 1, value: false, }); @@ -435,7 +427,7 @@ describe("updateStayAsSub", () => { }); }); -describe("leaveGroup", () => { +describe("leaveLfg", () => { beforeEach(async () => { await dbInsertUsers(3); }); @@ -444,26 +436,26 @@ describe("leaveGroup", () => { dbReset(); }); - test("removes member from group", async () => { + test("removes member from placeholder team", async () => { const tournament = await createTournament(); - const group = await createGroup(tournament.id, 1); + const team = await createPlaceholder(tournament.id, 1); await db - .insertInto("TournamentLFGGroupMember") + .insertInto("TournamentTeamMember") .values({ - groupId: group.id, - tournamentId: tournament.id, + tournamentTeamId: team.id, userId: 2, + isOwner: 0, role: "REGULAR", }) .execute(); - await TournamentLFGRepository.leaveGroup({ + await TournamentLFGRepository.leaveLfg({ userId: 2, tournamentId: tournament.id, }); - const groups = await TournamentLFGRepository.findGroupsByTournamentId( + const groups = await TournamentLFGRepository.findLookingTeamsByTournamentId( tournament.id, ); const members = groups[0].members; @@ -472,52 +464,52 @@ describe("leaveGroup", () => { expect(members[0].id).toBe(1); }); - test("deletes group when last member leaves", async () => { + test("deletes placeholder team when last member leaves", async () => { const tournament = await createTournament(); - await createGroup(tournament.id, 1); + await createPlaceholder(tournament.id, 1); - await TournamentLFGRepository.leaveGroup({ + await TournamentLFGRepository.leaveLfg({ userId: 1, tournamentId: tournament.id, }); - const groups = await TournamentLFGRepository.findGroupsByTournamentId( + const groups = await TournamentLFGRepository.findLookingTeamsByTournamentId( tournament.id, ); expect(groups).toHaveLength(0); }); - test("promotes MANAGER to OWNER when owner leaves", async () => { + test("promotes MANAGER to OWNER when owner leaves placeholder", async () => { const tournament = await createTournament(); - const group = await createGroup(tournament.id, 1); + const team = await createPlaceholder(tournament.id, 1); await db - .insertInto("TournamentLFGGroupMember") + .insertInto("TournamentTeamMember") .values({ - groupId: group.id, - tournamentId: tournament.id, + tournamentTeamId: team.id, userId: 2, + isOwner: 0, role: "MANAGER", }) .execute(); await db - .insertInto("TournamentLFGGroupMember") + .insertInto("TournamentTeamMember") .values({ - groupId: group.id, - tournamentId: tournament.id, + tournamentTeamId: team.id, userId: 3, + isOwner: 0, role: "REGULAR", }) .execute(); - await TournamentLFGRepository.leaveGroup({ + await TournamentLFGRepository.leaveLfg({ userId: 1, tournamentId: tournament.id, }); - const groups = await TournamentLFGRepository.findGroupsByTournamentId( + const groups = await TournamentLFGRepository.findLookingTeamsByTournamentId( tournament.id, ); const members = groups[0].members; @@ -526,31 +518,50 @@ describe("leaveGroup", () => { expect(newOwner?.role).toBe("OWNER"); }); - test("promotes first member to OWNER when owner leaves and no manager exists", async () => { + test("sets isLooking=0 for non-placeholder team", async () => { const tournament = await createTournament(); - const group = await createGroup(tournament.id, 1); + + const team = await db + .insertInto("TournamentTeam") + .values({ + tournamentId: tournament.id, + name: "Real Team", + inviteCode: "abc", + isLooking: 1, + isPlaceholder: 0, + chatCode: "chat123", + }) + .returning("id") + .executeTakeFirstOrThrow(); await db - .insertInto("TournamentLFGGroupMember") + .insertInto("TournamentTeamMember") .values({ - groupId: group.id, - tournamentId: tournament.id, - userId: 2, - role: "REGULAR", + tournamentTeamId: team.id, + userId: 1, + isOwner: 1, + role: "OWNER", }) .execute(); - await TournamentLFGRepository.leaveGroup({ + await TournamentLFGRepository.leaveLfg({ userId: 1, tournamentId: tournament.id, }); - const groups = await TournamentLFGRepository.findGroupsByTournamentId( + const groups = await TournamentLFGRepository.findLookingTeamsByTournamentId( tournament.id, ); - const members = groups[0].members; - expect(members[0].role).toBe("OWNER"); + expect(groups).toHaveLength(0); + + const teamRow = await db + .selectFrom("TournamentTeam") + .select("isLooking") + .where("id", "=", team.id) + .executeTakeFirstOrThrow(); + + expect(teamRow.isLooking).toBe(0); }); }); @@ -565,12 +576,12 @@ describe("getSubsForTournament", () => { test("returns userIds with isStayAsSub", async () => { const tournament = await createTournament(); - await TournamentLFGRepository.createGroup({ + await TournamentLFGRepository.createPlaceholderTeam({ tournamentId: tournament.id, userId: 1, isStayAsSub: true, }); - await createGroup(tournament.id, 2); + await createPlaceholder(tournament.id, 2); const subs = await TournamentLFGRepository.getSubsForTournament( tournament.id, @@ -581,7 +592,7 @@ describe("getSubsForTournament", () => { test("returns empty when nobody opted in", async () => { const tournament = await createTournament(); - await createGroup(tournament.id, 1); + await createPlaceholder(tournament.id, 1); const subs = await TournamentLFGRepository.getSubsForTournament( tournament.id, @@ -590,46 +601,3 @@ describe("getSubsForTournament", () => { expect(subs).toHaveLength(0); }); }); - -describe("cleanupForTournamentStart", () => { - beforeEach(async () => { - await dbInsertUsers(2); - }); - - afterEach(() => { - dbReset(); - }); - - test("deletes all groups for a tournament", async () => { - const tournament = await createTournament(); - await createGroup(tournament.id, 1); - await createGroup(tournament.id, 2); - - await TournamentLFGRepository.cleanupForTournamentStart(tournament.id); - - const groups = await TournamentLFGRepository.findGroupsByTournamentId( - tournament.id, - ); - - expect(groups).toHaveLength(0); - }); - - test("does not affect groups from other tournaments", async () => { - const tournament1 = await createTournament(); - const tournament2 = await createTournament(); - await createGroup(tournament1.id, 1); - await createGroup(tournament2.id, 2); - - await TournamentLFGRepository.cleanupForTournamentStart(tournament1.id); - - const groups1 = await TournamentLFGRepository.findGroupsByTournamentId( - tournament1.id, - ); - const groups2 = await TournamentLFGRepository.findGroupsByTournamentId( - tournament2.id, - ); - - expect(groups1).toHaveLength(0); - expect(groups2).toHaveLength(1); - }); -}); diff --git a/app/features/tournament-lfg/TournamentLFGRepository.server.ts b/app/features/tournament-lfg/TournamentLFGRepository.server.ts index 99062f99f..5b186953f 100644 --- a/app/features/tournament-lfg/TournamentLFGRepository.server.ts +++ b/app/features/tournament-lfg/TournamentLFGRepository.server.ts @@ -9,68 +9,48 @@ import { userChatNameColorForJson, } from "~/utils/kysely.server"; import { errorIsSqliteForeignKeyConstraintFailure } from "~/utils/sql"; +import { randomTeamName } from "~/utils/team-name"; -type CreateGroupFromTeamArgs = { - tournamentId: number; - tournamentTeamId: number; - members: Array<{ userId: number; isOwner: boolean }>; -}; -export function createGroupFromTeam(args: CreateGroupFromTeamArgs) { - return db.transaction().execute(async (trx) => { - const createdGroup = await trx - .insertInto("TournamentLFGGroup") - .values({ - tournamentId: args.tournamentId, - tournamentTeamId: args.tournamentTeamId, - chatCode: shortNanoid(), - }) - .returning("id") - .executeTakeFirstOrThrow(); - - for (const member of args.members) { - await trx - .insertInto("TournamentLFGGroupMember") - .values({ - groupId: createdGroup.id, - tournamentId: args.tournamentId, - userId: member.userId, - role: member.isOwner ? "OWNER" : "REGULAR", - }) - .execute(); - } - - return createdGroup; - }); +export function startLooking(teamId: number) { + return db + .updateTable("TournamentTeam") + .set({ isLooking: 1, chatCode: shortNanoid() }) + .where("id", "=", teamId) + .execute(); } -type CreateGroupArgs = { +type CreatePlaceholderTeamArgs = { tournamentId: number; userId: number; isStayAsSub?: boolean; }; -export function createGroup(args: CreateGroupArgs) { +export function createPlaceholderTeam(args: CreatePlaceholderTeamArgs) { return db.transaction().execute(async (trx) => { - const createdGroup = await trx - .insertInto("TournamentLFGGroup") + const createdTeam = await trx + .insertInto("TournamentTeam") .values({ tournamentId: args.tournamentId, + name: randomTeamName(), + inviteCode: shortNanoid(), + isPlaceholder: 1, + isLooking: 1, chatCode: shortNanoid(), }) .returning("id") .executeTakeFirstOrThrow(); await trx - .insertInto("TournamentLFGGroupMember") + .insertInto("TournamentTeamMember") .values({ - groupId: createdGroup.id, - tournamentId: args.tournamentId, + tournamentTeamId: createdTeam.id, userId: args.userId, + isOwner: 1, role: "OWNER", isStayAsSub: args.isStayAsSub ? 1 : 0, }) .execute(); - return createdGroup; + return createdTeam; }); } @@ -83,39 +63,34 @@ type TournamentLFGMemberObject = { languages: Tables["User"]["languages"]; vc: Tables["User"]["vc"]; pronouns: Tables["User"]["pronouns"]; - role: Tables["TournamentLFGGroupMember"]["role"]; - note: Tables["TournamentLFGGroupMember"]["note"]; - isStayAsSub: Tables["TournamentLFGGroupMember"]["isStayAsSub"]; + role: Tables["TournamentTeamMember"]["role"]; + isStayAsSub: Tables["TournamentTeamMember"]["isStayAsSub"]; weapons: Tables["User"]["qWeaponPool"]; chatNameColor: string | null; plusTier: Tables["PlusTier"]["tier"] | null; }; -export async function findGroupsByTournamentId(tournamentId: number) { +export async function findLookingTeamsByTournamentId(tournamentId: number) { return db - .selectFrom("TournamentLFGGroup") + .selectFrom("TournamentTeam") .innerJoin( - "TournamentLFGGroupMember", - "TournamentLFGGroupMember.groupId", - "TournamentLFGGroup.id", - ) - .innerJoin("User", "User.id", "TournamentLFGGroupMember.userId") - .leftJoin("PlusTier", "PlusTier.userId", "User.id") - .leftJoin( - "TournamentTeam", + "TournamentTeamMember", + "TournamentTeamMember.tournamentTeamId", "TournamentTeam.id", - "TournamentLFGGroup.tournamentTeamId", ) + .innerJoin("User", "User.id", "TournamentTeamMember.userId") + .leftJoin("PlusTier", "PlusTier.userId", "User.id") .leftJoin( "UserSubmittedImage", "UserSubmittedImage.id", "TournamentTeam.avatarImgId", ) .select(({ fn, eb }) => [ - "TournamentLFGGroup.id", - "TournamentLFGGroup.chatCode", - "TournamentLFGGroup.tournamentTeamId", - "TournamentLFGGroup.visibility", + "TournamentTeam.id", + "TournamentTeam.chatCode", + "TournamentTeam.isPlaceholder", + "TournamentTeam.lfgVisibility as visibility", + "TournamentTeam.lfgNote as note", "TournamentTeam.name as teamName", concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as( "teamAvatarUrl", @@ -131,9 +106,8 @@ export async function findGroupsByTournamentId(tournamentId: number) { languages: eb.ref("User.languages"), vc: eb.ref("User.vc"), pronouns: eb.ref("User.pronouns"), - role: eb.ref("TournamentLFGGroupMember.role"), - note: eb.ref("TournamentLFGGroupMember.note"), - isStayAsSub: eb.ref("TournamentLFGGroupMember.isStayAsSub"), + role: eb.ref("TournamentTeamMember.role"), + isStayAsSub: eb.ref("TournamentTeamMember.isStayAsSub"), weapons: eb.ref("User.qWeaponPool"), chatNameColor: userChatNameColorForJson, plusTier: eb.ref("PlusTier.tier"), @@ -142,101 +116,74 @@ export async function findGroupsByTournamentId(tournamentId: number) { .$castTo() .as("members"), ]) - .where("TournamentLFGGroup.tournamentId", "=", tournamentId) - .groupBy("TournamentLFGGroup.id") + .where("TournamentTeam.tournamentId", "=", tournamentId) + .where("TournamentTeam.isLooking", "=", 1) + .groupBy("TournamentTeam.id") .execute(); } -export function morphGroups({ - survivingGroupId, - otherGroupId, +// xxx: ensure that the team size does not exceed the max, also filter out in frontend +export function mergeTeams({ + survivingTeamId, + otherTeamId, maxGroupSize, - tournamentId, }: { - survivingGroupId: number; - otherGroupId: number; + survivingTeamId: number; + otherTeamId: number; maxGroupSize: number; - tournamentId: number; }) { return db.transaction().execute(async (trx) => { - const survivingGroup = await trx - .selectFrom("TournamentLFGGroup") - .select("tournamentTeamId") - .where("id", "=", survivingGroupId) - .executeTakeFirstOrThrow(); - - if (!survivingGroup.tournamentTeamId) { - const createdTeam = await trx - .insertInto("TournamentTeam") - .values({ - tournamentId, - name: `LFG Team ${shortNanoid()}`, - inviteCode: "", - }) - .returning("id") - .executeTakeFirstOrThrow(); - - await trx - .updateTable("TournamentLFGGroup") - .set({ tournamentTeamId: createdTeam.id }) - .where("id", "=", survivingGroupId) - .execute(); - } - await trx - .updateTable("TournamentLFGGroup") + .updateTable("TournamentTeam") .set({ chatCode: shortNanoid() }) - .where("TournamentLFGGroup.id", "=", survivingGroupId) + .where("TournamentTeam.id", "=", survivingTeamId) .execute(); - const otherGroupMembers = await trx - .selectFrom("TournamentLFGGroupMember") - .select([ - "TournamentLFGGroupMember.userId", - "TournamentLFGGroupMember.role", - ]) - .where("TournamentLFGGroupMember.groupId", "=", otherGroupId) + const otherMembers = await trx + .selectFrom("TournamentTeamMember") + .select(["TournamentTeamMember.userId", "TournamentTeamMember.role"]) + .where("TournamentTeamMember.tournamentTeamId", "=", otherTeamId) .execute(); - for (const member of otherGroupMembers) { + for (const member of otherMembers) { await trx - .updateTable("TournamentLFGGroupMember") + .updateTable("TournamentTeamMember") .set({ role: member.role === "OWNER" ? "MANAGER" : member.role, - groupId: survivingGroupId, + tournamentTeamId: survivingTeamId, }) - .where("TournamentLFGGroupMember.groupId", "=", otherGroupId) - .where("TournamentLFGGroupMember.userId", "=", member.userId) + .where("TournamentTeamMember.tournamentTeamId", "=", otherTeamId) + .where("TournamentTeamMember.userId", "=", member.userId) .execute(); } - await deleteLikesByGroupId(survivingGroupId, trx); + await deleteLikesByTeamId(survivingTeamId, trx); await trx - .deleteFrom("TournamentLFGGroup") - .where("TournamentLFGGroup.id", "=", otherGroupId) + .deleteFrom("TournamentTeam") + .where("TournamentTeam.id", "=", otherTeamId) .execute(); invariant( - await isGroupCorrect(survivingGroupId, trx, maxGroupSize), + await isTeamCorrect(survivingTeamId, trx, maxGroupSize), "Group has too many members after merge", ); }); } export async function addLike({ - likerGroupId, - targetGroupId, + likerTeamId, + targetTeamId, }: { - likerGroupId: number; - targetGroupId: number; + likerTeamId: number; + targetTeamId: number; }) { try { await db .insertInto("TournamentLFGLike") - .values({ likerGroupId, targetGroupId }) + .values({ likerTeamId, targetTeamId }) .onConflict((oc) => - oc.columns(["likerGroupId", "targetGroupId"]).doNothing(), + oc.columns(["likerTeamId", "targetTeamId"]).doNothing(), ) .execute(); } catch (error) { @@ -248,100 +195,94 @@ export async function addLike({ } export function deleteLike({ - likerGroupId, - targetGroupId, + likerTeamId, + targetTeamId, }: { - likerGroupId: number; - targetGroupId: number; + likerTeamId: number; + targetTeamId: number; }) { return db .deleteFrom("TournamentLFGLike") - .where("likerGroupId", "=", likerGroupId) - .where("targetGroupId", "=", targetGroupId) + .where("likerTeamId", "=", likerTeamId) + .where("targetTeamId", "=", targetTeamId) .execute(); } -export async function allLikesByGroupId(groupId: number) { +export async function allLikesByTeamId(teamId: number) { const rows = await db .selectFrom("TournamentLFGLike") - .select([ - "TournamentLFGLike.likerGroupId", - "TournamentLFGLike.targetGroupId", - ]) + .select(["TournamentLFGLike.likerTeamId", "TournamentLFGLike.targetTeamId"]) .where((eb) => eb.or([ - eb("TournamentLFGLike.likerGroupId", "=", groupId), - eb("TournamentLFGLike.targetGroupId", "=", groupId), + eb("TournamentLFGLike.likerTeamId", "=", teamId), + eb("TournamentLFGLike.targetTeamId", "=", teamId), ]), ) .execute(); return { given: rows - .filter((row) => row.likerGroupId === groupId) - .map((row) => ({ groupId: row.targetGroupId })), + .filter((row) => row.likerTeamId === teamId) + .map((row) => ({ teamId: row.targetTeamId })), received: rows - .filter((row) => row.targetGroupId === groupId) - .map((row) => ({ groupId: row.likerGroupId })), + .filter((row) => row.targetTeamId === teamId) + .map((row) => ({ teamId: row.likerTeamId })), }; } -export function updateMemberNote({ - groupId, - userId, +export function updateTeamNote({ + teamId, value, }: { - groupId: number; - userId: number; + teamId: number; value: string | null; }) { return db - .updateTable("TournamentLFGGroupMember") - .set({ note: value }) - .where("groupId", "=", groupId) - .where("userId", "=", userId) + .updateTable("TournamentTeam") + .set({ lfgNote: value }) + .where("id", "=", teamId) .execute(); } export function updateMemberRole({ userId, - groupId, + teamId, role, }: { userId: number; - groupId: number; - role: Tables["TournamentLFGGroupMember"]["role"]; + teamId: number; + role: Tables["TournamentTeamMember"]["role"]; }) { if (role === "OWNER") { throw new Error("Can't set role to OWNER with this function"); } return db - .updateTable("TournamentLFGGroupMember") + .updateTable("TournamentTeamMember") .set({ role }) .where("userId", "=", userId) - .where("groupId", "=", groupId) + .where("tournamentTeamId", "=", teamId) .execute(); } export function updateStayAsSub({ - groupId, + teamId, userId, value, }: { - groupId: number; + teamId: number; userId: number; value: boolean; }) { return db - .updateTable("TournamentLFGGroupMember") + .updateTable("TournamentTeamMember") .set({ isStayAsSub: value ? 1 : 0 }) - .where("groupId", "=", groupId) + .where("tournamentTeamId", "=", teamId) .where("userId", "=", userId) .execute(); } -export function leaveGroup({ +export function leaveLfg({ userId, tournamentId, }: { @@ -349,90 +290,106 @@ export function leaveGroup({ tournamentId: number; }) { return db.transaction().execute(async (trx) => { - const userGroup = await trx - .selectFrom("TournamentLFGGroupMember") + const userTeam = await trx + .selectFrom("TournamentTeamMember") + .innerJoin( + "TournamentTeam", + "TournamentTeam.id", + "TournamentTeamMember.tournamentTeamId", + ) .select([ - "TournamentLFGGroupMember.groupId", - "TournamentLFGGroupMember.role", + "TournamentTeamMember.tournamentTeamId", + "TournamentTeamMember.role", + "TournamentTeam.isPlaceholder", ]) - .where("TournamentLFGGroupMember.userId", "=", userId) - .where("TournamentLFGGroupMember.tournamentId", "=", tournamentId) + .where("TournamentTeamMember.userId", "=", userId) + .where("TournamentTeam.tournamentId", "=", tournamentId) + .where("TournamentTeam.isLooking", "=", 1) .executeTakeFirstOrThrow(); - await trx - .deleteFrom("TournamentLFGGroupMember") - .where("userId", "=", userId) - .where("groupId", "=", userGroup.groupId) - .execute(); - - const remainingMembers = await trx - .selectFrom("TournamentLFGGroupMember") - .select(["userId", "role"]) - .where("groupId", "=", userGroup.groupId) - .execute(); - - if (remainingMembers.length === 0) { + if (!userTeam.isPlaceholder) { await trx - .deleteFrom("TournamentLFGGroup") - .where("id", "=", userGroup.groupId) + .updateTable("TournamentTeam") + .set({ isLooking: 0 }) + .where("id", "=", userTeam.tournamentTeamId) .execute(); return; } - if (userGroup.role === "OWNER") { + // xxx: or just call TournamentTeam.unregister? + // xxx: also leaving team should not be possible via this so remove that code + await trx + .deleteFrom("TournamentTeamMember") + .where("userId", "=", userId) + .where("tournamentTeamId", "=", userTeam.tournamentTeamId) + .execute(); + + const remainingMembers = await trx + .selectFrom("TournamentTeamMember") + .select(["userId", "role"]) + .where("tournamentTeamId", "=", userTeam.tournamentTeamId) + .execute(); + + if (remainingMembers.length === 0) { + await trx + .deleteFrom("TournamentTeam") + .where("id", "=", userTeam.tournamentTeamId) + .execute(); + return; + } + + if (userTeam.role === "OWNER") { const newOwner = remainingMembers.find((m) => m.role === "MANAGER") ?? remainingMembers[0]; await trx - .updateTable("TournamentLFGGroupMember") + .updateTable("TournamentTeamMember") .set({ role: "OWNER" }) .where("userId", "=", newOwner.userId) - .where("groupId", "=", userGroup.groupId) + .where("tournamentTeamId", "=", userTeam.tournamentTeamId) .execute(); } }); } -export function cleanupForTournamentStart(tournamentId: number) { - return db - .deleteFrom("TournamentLFGGroup") - .where("TournamentLFGGroup.tournamentId", "=", tournamentId) - .execute(); -} - export async function getSubsForTournament(tournamentId: number) { const rows = await db - .selectFrom("TournamentLFGGroupMember") - .select("TournamentLFGGroupMember.userId") - .where("TournamentLFGGroupMember.tournamentId", "=", tournamentId) - .where("TournamentLFGGroupMember.isStayAsSub", "=", 1) + .selectFrom("TournamentTeamMember") + .innerJoin( + "TournamentTeam", + "TournamentTeam.id", + "TournamentTeamMember.tournamentTeamId", + ) + .select("TournamentTeamMember.userId") + .where("TournamentTeam.tournamentId", "=", tournamentId) + .where("TournamentTeamMember.isStayAsSub", "=", 1) .execute(); return rows.map((row) => row.userId); } -function deleteLikesByGroupId(groupId: number, trx: Transaction) { +function deleteLikesByTeamId(teamId: number, trx: Transaction) { return trx .deleteFrom("TournamentLFGLike") .where((eb) => eb.or([ - eb("TournamentLFGLike.likerGroupId", "=", groupId), - eb("TournamentLFGLike.targetGroupId", "=", groupId), + eb("TournamentLFGLike.likerTeamId", "=", teamId), + eb("TournamentLFGLike.targetTeamId", "=", teamId), ]), ) .execute(); } -async function isGroupCorrect( - groupId: number, +async function isTeamCorrect( + teamId: number, trx: Transaction, maxGroupSize: number, ): Promise { const members = await trx - .selectFrom("TournamentLFGGroupMember") - .select("TournamentLFGGroupMember.userId") - .where("TournamentLFGGroupMember.groupId", "=", groupId) + .selectFrom("TournamentTeamMember") + .select("TournamentTeamMember.userId") + .where("TournamentTeamMember.tournamentTeamId", "=", teamId) .execute(); return members.length <= maxGroupSize; diff --git a/app/features/tournament-lfg/actions/to.$id.looking.server.ts b/app/features/tournament-lfg/actions/to.$id.looking.server.ts index cbf27529f..1d19d4751 100644 --- a/app/features/tournament-lfg/actions/to.$id.looking.server.ts +++ b/app/features/tournament-lfg/actions/to.$id.looking.server.ts @@ -10,7 +10,7 @@ import { assertUnreachable } from "~/utils/types"; import { idObject } from "~/utils/zod"; import * as TournamentLFGRepository from "../TournamentLFGRepository.server"; import { lookingSchema } from "../tournament-lfg-schemas.server"; -import { survivingGroupId } from "../tournament-lfg-utils"; +import { survivingTeamId } from "../tournament-lfg-utils"; // xxx: prevent actions in certain cases? like when tournament has started export const action = async ({ request, params }: ActionFunctionArgs) => { @@ -23,7 +23,9 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const findOwnGroup = async () => { const groups = - await TournamentLFGRepository.findGroupsByTournamentId(tournamentId); + await TournamentLFGRepository.findLookingTeamsByTournamentId( + tournamentId, + ); return groups.find((g) => g.members.some((m) => m.id === user.id)); }; @@ -49,28 +51,9 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const team = tournament.teamMemberOfByUser(user); if (team) { - const groups = - await TournamentLFGRepository.findGroupsByTournamentId(tournamentId); - const teamAlreadyLinked = groups.some( - (g) => g.tournamentTeamId === team.id, - ); - if (teamAlreadyLinked) return null; - - const memberAlreadyInGroup = team.members.some((m) => - groups.some((g) => g.members.some((gm) => gm.id === m.userId)), - ); - if (memberAlreadyInGroup) return null; - - await TournamentLFGRepository.createGroupFromTeam({ - tournamentId, - tournamentTeamId: team.id, - members: team.members.map((m) => ({ - userId: m.userId, - isOwner: Boolean(m.isOwner), - })), - }); + await TournamentLFGRepository.startLooking(team.id); } else { - await TournamentLFGRepository.createGroup({ + await TournamentLFGRepository.createPlaceholderTeam({ tournamentId, userId: user.id, isStayAsSub: data.stayAsSub ?? false, @@ -84,8 +67,8 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { if (!ownGroup || !isGroupManager(ownGroup)) return null; await TournamentLFGRepository.addLike({ - likerGroupId: ownGroup.id, - targetGroupId: data.targetGroupId, + likerTeamId: ownGroup.id, + targetTeamId: data.targetTeamId, }); break; @@ -95,44 +78,48 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { if (!ownGroup || !isGroupManager(ownGroup)) return null; await TournamentLFGRepository.deleteLike({ - likerGroupId: ownGroup.id, - targetGroupId: data.targetGroupId, + likerTeamId: ownGroup.id, + targetTeamId: data.targetTeamId, }); break; } case "ACCEPT": { const groups = - await TournamentLFGRepository.findGroupsByTournamentId(tournamentId); + await TournamentLFGRepository.findLookingTeamsByTournamentId( + tournamentId, + ); const ownGroup = groups.find((g) => g.members.some((m) => m.id === user.id), ); if (!ownGroup || !isGroupManager(ownGroup)) return null; - const theirGroup = groups.find((g) => g.id === data.targetGroupId); + const theirGroup = groups.find((g) => g.id === data.targetTeamId); if (!theirGroup) return null; - const theirLikes = await TournamentLFGRepository.allLikesByGroupId( - data.targetGroupId, + const theirLikes = await TournamentLFGRepository.allLikesByTeamId( + data.targetTeamId, ); - if (!theirLikes.given.some((like) => like.groupId === ownGroup.id)) { + if (!theirLikes.given.some((like) => like.teamId === ownGroup.id)) { return null; } - const surviving = survivingGroupId({ + const surviving = survivingTeamId({ ourGroup: { id: ownGroup.id, - tournamentTeamId: ownGroup.tournamentTeamId, + isPlaceholder: Boolean(ownGroup.isPlaceholder), teamName: null, teamAvatarUrl: null, + note: null, members: [], usersRole: null, }, theirGroup: { id: theirGroup.id, - tournamentTeamId: theirGroup.tournamentTeamId, + isPlaceholder: Boolean(theirGroup.isPlaceholder), teamName: null, teamAvatarUrl: null, + note: null, members: [], usersRole: null, }, @@ -145,11 +132,10 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { user, }); - await TournamentLFGRepository.morphGroups({ - survivingGroupId: surviving, - otherGroupId: otherGroup.id, + await TournamentLFGRepository.mergeTeams({ + survivingTeamId: surviving, + otherTeamId: otherGroup.id, maxGroupSize: tournament.maxMembersPerTeam, - tournamentId, }); break; @@ -159,7 +145,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { errorToastIfFalsy(ownGroup && isGroupOwner(ownGroup), "Not owner"); await TournamentLFGRepository.updateMemberRole({ - groupId: ownGroup!.id, + teamId: ownGroup!.id, userId: data.userId, role: "MANAGER", }); @@ -171,7 +157,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { errorToastIfFalsy(ownGroup && isGroupOwner(ownGroup), "Not owner"); await TournamentLFGRepository.updateMemberRole({ - groupId: ownGroup!.id, + teamId: ownGroup!.id, userId: data.userId, role: "REGULAR", }); @@ -182,9 +168,8 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const ownGroup = await findOwnGroup(); if (!ownGroup) return null; - await TournamentLFGRepository.updateMemberNote({ - groupId: ownGroup.id, - userId: user.id, + await TournamentLFGRepository.updateTeamNote({ + teamId: ownGroup.id, value: data.value, }); @@ -195,7 +180,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { if (!ownGroup) return null; await TournamentLFGRepository.updateStayAsSub({ - groupId: ownGroup.id, + teamId: ownGroup.id, userId: user.id, value: data.value, }); @@ -203,7 +188,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { break; } case "LEAVE_GROUP": { - await TournamentLFGRepository.leaveGroup({ + await TournamentLFGRepository.leaveLfg({ userId: user.id, tournamentId, }); diff --git a/app/features/tournament-lfg/components/LFGGroupCard.tsx b/app/features/tournament-lfg/components/LFGGroupCard.tsx index 6bcafa140..69e8da5a9 100644 --- a/app/features/tournament-lfg/components/LFGGroupCard.tsx +++ b/app/features/tournament-lfg/components/LFGGroupCard.tsx @@ -31,7 +31,6 @@ export type LFGGroupMember = { vc: "YES" | "NO" | "LISTEN_ONLY" | null; pronouns: Pronouns | null; role: "OWNER" | "MANAGER" | "REGULAR"; - note: string | null; isStayAsSub: boolean; weapons: Array<{ weaponSplId: MainWeaponId; isFavorite: boolean }> | null; chatNameColor: string | null; @@ -40,13 +39,15 @@ export type LFGGroupMember = { export type LFGGroup = { id: number; - tournamentTeamId: number | null; + isPlaceholder: boolean; teamName: string | null; teamAvatarUrl: string | null; + note: string | null; members: LFGGroupMember[]; usersRole: "OWNER" | "MANAGER" | "REGULAR" | null; }; +// xxx: make sure we don't show team name if isPlaceholder // xxx: "cancel" & "save" buttons off export function LFGGroupCard({ group, @@ -65,14 +66,6 @@ export function LFGGroupCard({ return (
- {/* {group.teamName ? ( -
- {group.teamAvatarUrl ? ( - - ) : null} -
{group.teamName}
-
- ) : null} */} {group.teamName ? ( {group.teamAvatarUrl ? ( @@ -91,10 +84,13 @@ export function LFGGroupCard({ /> ))} + {isOwnGroup ? ( + + ) : null} {action && (ownGroup?.usersRole === "OWNER" || ownGroup?.usersRole === "MANAGER") ? ( - +
@@ -171,14 +164,11 @@ function LFGGroupMemberRow({
) : null} - {isOwnGroup ? ( - - ) : null} ); } -function LFGMemberNote({ +function LFGTeamNote({ note, editable, }: { diff --git a/app/features/tournament-lfg/loaders/to.$id.looking.server.ts b/app/features/tournament-lfg/loaders/to.$id.looking.server.ts index 8b81b89f3..9dad8d51b 100644 --- a/app/features/tournament-lfg/loaders/to.$id.looking.server.ts +++ b/app/features/tournament-lfg/loaders/to.$id.looking.server.ts @@ -13,16 +13,17 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { const { id: tournamentId } = parseParams({ params, schema: idObject }); const rawGroups = - await TournamentLFGRepository.findGroupsByTournamentId(tournamentId); + await TournamentLFGRepository.findLookingTeamsByTournamentId(tournamentId); const groups: LFGGroup[] = rawGroups.map((group) => { const members = transformMembers(group.members); return { id: group.id, - tournamentTeamId: group.tournamentTeamId, + isPlaceholder: Boolean(group.isPlaceholder), teamName: group.teamName ?? null, teamAvatarUrl: group.teamAvatarUrl ?? null, + note: group.note ?? null, members, usersRole: members.find((m) => m.id === user?.id)?.role ?? null, }; @@ -34,14 +35,13 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { const otherGroups = groups.filter((g) => g.id !== ownGroup?.id); const likes = ownGroup - ? await TournamentLFGRepository.allLikesByGroupId(ownGroup.id) + ? await TournamentLFGRepository.allLikesByTeamId(ownGroup.id) : { given: [], received: [] }; const ownTeam = await resolveOwnTeam({ user, tournamentId, ownGroup, - groups, }); return { @@ -58,12 +58,10 @@ async function resolveOwnTeam({ user, tournamentId, ownGroup, - groups, }: { user: ReturnType; tournamentId: number; ownGroup: LFGGroup | null; - groups: LFGGroup[]; }): Promise { if (!user) return null; if (ownGroup) return null; @@ -76,9 +74,6 @@ async function resolveOwnTeam({ const team = tournament.teamMemberOfByUser(user); if (!team) return null; - const teamAlreadyLinked = groups.some((g) => g.tournamentTeamId === team.id); - if (teamAlreadyLinked) return null; - const members: LFGGroupMember[] = team.members.map((m) => ({ id: m.userId, username: m.username, @@ -89,7 +84,6 @@ async function resolveOwnTeam({ vc: null, pronouns: null, role: m.isOwner ? "OWNER" : "REGULAR", - note: null, isStayAsSub: false, weapons: null, chatNameColor: null, @@ -97,10 +91,11 @@ async function resolveOwnTeam({ })); return { - id: -1, - tournamentTeamId: team.id, + id: team.id, + isPlaceholder: false, teamName: team.name, teamAvatarUrl: team.pickupAvatarUrl, + note: null, members, usersRole: members.find((m) => m.id === user.id)?.role ?? null, }; @@ -108,7 +103,7 @@ async function resolveOwnTeam({ function transformMembers( rawMembers: Awaited< - ReturnType + ReturnType >[number]["members"], ): LFGGroupMember[] { return rawMembers.map((m) => { @@ -130,7 +125,6 @@ function transformMembers( vc: m.vc, pronouns, role: m.role, - note: m.note, isStayAsSub: m.isStayAsSub === 1, weapons, chatNameColor: m.chatNameColor, diff --git a/app/features/tournament-lfg/routes/to.$id.looking.tsx b/app/features/tournament-lfg/routes/to.$id.looking.tsx index d7cb640c2..f356d1917 100644 --- a/app/features/tournament-lfg/routes/to.$id.looking.tsx +++ b/app/features/tournament-lfg/routes/to.$id.looking.tsx @@ -58,17 +58,17 @@ function Groups() { const neutralGroups = data.groups.filter( (group) => - !data.likes.given.some((like) => like.groupId === group.id) && - !data.likes.received.some((like) => like.groupId === group.id), + !data.likes.given.some((like) => like.teamId === group.id) && + !data.likes.received.some((like) => like.teamId === group.id), ); const likedGroups = data.groups.filter((group) => - data.likes.given.some((like) => like.groupId === group.id), + data.likes.given.some((like) => like.teamId === group.id), ); const groupsReceivedLikesFrom = data.groups.filter((group) => - data.likes.received.some((like) => like.groupId === group.id), + data.likes.received.some((like) => like.teamId === group.id), ); - const flipKey = `${neutralGroups.map((g) => `${g.id}-${isMobile ? true : data.likes.given.some((l) => l.groupId === g.id)}`).join(":")}:${groupsReceivedLikesFrom.map((g) => g.id).join(":")}`; + const flipKey = `${neutralGroups.map((g) => `${g.id}-${isMobile ? true : data.likes.given.some((l) => l.teamId === g.id)}`).join(":")}:${groupsReceivedLikesFrom.map((g) => g.id).join(":")}`; const invitedGroupsDesktop = (
@@ -157,7 +157,7 @@ function Groups() { ? data.groups.filter( (group) => !data.likes.received.some( - (like) => like.groupId === group.id, + (like) => like.teamId === group.id, ), ) : neutralGroups @@ -166,7 +166,7 @@ function Groups() { key={group.id} group={group} action={ - data.likes.given.some((like) => like.groupId === group.id) + data.likes.given.some((like) => like.teamId === group.id) ? "UNLIKE" : "LIKE" } diff --git a/app/features/tournament-lfg/tournament-lfg-schemas.server.ts b/app/features/tournament-lfg/tournament-lfg-schemas.server.ts index 0a3dae5b0..6df7eea99 100644 --- a/app/features/tournament-lfg/tournament-lfg-schemas.server.ts +++ b/app/features/tournament-lfg/tournament-lfg-schemas.server.ts @@ -7,15 +7,15 @@ export const lookingSchema = z.union([ joinQueueFormSchema, z.object({ _action: _action("LIKE"), - targetGroupId: id, + targetTeamId: id, }), z.object({ _action: _action("UNLIKE"), - targetGroupId: id, + targetTeamId: id, }), z.object({ _action: _action("ACCEPT"), - targetGroupId: id, + targetTeamId: id, }), z.object({ _action: _action("GIVE_MANAGER"), diff --git a/app/features/tournament-lfg/tournament-lfg-utils.ts b/app/features/tournament-lfg/tournament-lfg-utils.ts index a26c92faa..2e568629c 100644 --- a/app/features/tournament-lfg/tournament-lfg-utils.ts +++ b/app/features/tournament-lfg/tournament-lfg-utils.ts @@ -1,20 +1,16 @@ import type { LFGGroup } from "./components/LFGGroupCard"; -export function findOwnGroup(groups: LFGGroup[], userId: number) { - return groups.find((g) => g.members.some((m) => m.id === userId)) ?? null; -} - -export function survivingGroupId({ +export function survivingTeamId({ ourGroup, theirGroup, }: { ourGroup: LFGGroup; theirGroup: LFGGroup; }) { - if (ourGroup.tournamentTeamId && !theirGroup.tournamentTeamId) { + if (!ourGroup.isPlaceholder && theirGroup.isPlaceholder) { return ourGroup.id; } - if (!ourGroup.tournamentTeamId && theirGroup.tournamentTeamId) { + if (ourGroup.isPlaceholder && !theirGroup.isPlaceholder) { return theirGroup.id; } diff --git a/app/features/tournament/TournamentRepository.server.ts b/app/features/tournament/TournamentRepository.server.ts index 567efb7ef..be49e36c2 100644 --- a/app/features/tournament/TournamentRepository.server.ts +++ b/app/features/tournament/TournamentRepository.server.ts @@ -251,6 +251,7 @@ export async function findById(id: number) { ).as("team"), ]) .where("TournamentTeam.tournamentId", "=", id) + .where("TournamentTeam.isPlaceholder", "=", 0) .orderBy("TournamentTeam.seed", "asc") .orderBy("TournamentTeam.createdAt", "asc") .orderBy("TournamentTeam.id", "asc"), @@ -484,6 +485,7 @@ export function forShowcase() { ), ) .whereRef("TournamentTeam.tournamentId", "=", "Tournament.id") + .where("TournamentTeam.isPlaceholder", "=", 0) .where((eb) => eb.or([ eb("TournamentTeamCheckIn.checkedInAt", "is not", null), diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx index a844ab6e2..084527a3e 100644 --- a/app/features/tournament/routes/to.$id.register.tsx +++ b/app/features/tournament/routes/to.$id.register.tsx @@ -69,6 +69,8 @@ import { import { useTournament } from "./to.$id"; export { loader, action }; +// xxx: handle placeholder team made +// xxx: allow promoting members to managers export default function TournamentRegisterPage() { const isMounted = useIsMounted(); const tournament = useTournament(); diff --git a/app/utils/team-name-data.ts b/app/utils/team-name-data.ts new file mode 100644 index 000000000..e339f4112 --- /dev/null +++ b/app/utils/team-name-data.ts @@ -0,0 +1,2037 @@ +// Auto-generated team name data from adj.json and sub.json +// Regenerate with: npx tsx scripts/generate-team-names.ts + +export const ADJECTIVES = [ + "Inkless", + "Clocked-Out", + "Attention-Seeking", + "Southpaw", + "Splatsville's", + "Big-Money", + "Normcore", + "Faithful", + "Defensive", + "Champion", + "Royal", + "Ammo Knights", + "Naut Couture", + "Man-o'-Wardrobe", + "Crush Station", + "Hotlantis", + "Tableturf Battling", + "Research-Conducting", + "Apprentice", + "Part-Time", + "Go-Gettin'", + "Overachieving", + "Profreshional", + "Legendary", + "Mid-Rush", + "High-Tide", + "Cohock-Chargin'", + "Foggy", + "Low-Tide", + "Fall-Lovin'", + "Spring-Lovin'", + "Winter-Lovin'", + "Summer-Lovin'", + "Blissed-Out", + "Trendy", + "Sharp-Edged", + "Stylin'", + "Cool", + "Basic", + "Traditional", + "Trendsetting", + "All-Natural", + "Sizzlin'", + "Day-Seizing", + "Fall-Themed", + "Spring-Fresh", + "Captivating", + "The", + "Low-Rolling", + "High-Rolling", + "Commanding", + "Wannabe", + "Deadeye", + "Reckless", + "Chilled-Out", + "Silent", + "Ambidextrous", + "Stand-Up", + "Livestreaming", + "Meditative", + "Right-Handed", + "Low-Key", + "Pro", + "Amateur", + "Inkopolis Square's", + "Inkopolis's Own", + "Inkopolitan", + "Feral", + "Downtown", + "Country-Born", + "Greedy", + "Trending", + "Shortcut-Taking", + "Fluid", + "Optimistic", + "Daydreamin'", + "Clever", + "Hungry, Hungry", + "Competitive", + "Slapdash", + "Half-Hearted", + "Early-Bird", + "Daring", + "Hungry", + "Athletic", + "Early-Morning", + "Inkslinging", + "Up-and-at-'Em", + "Deep-Sea", + "Rise-and-Shine", + "Beachy", + "Freewheelin'", + "Old-Fashioned", + "World's Best", + "National", + "Stormy", + "Lonely", + "Groundbreaking", + "Assertive", + "Super-Scary", + "Vacationing", + "Nostalgic", + "Homecoming", + "Hard-Workin'", + "Sweet-Toothed", + "Seafaring", + "Self-Taught", + "Summer-Breaking", + "Processed", + "Not-So-Fresh", + "Salty", + "Destined", + "Gullible", + "Wiped-Out", + "Rockin'", + "Retro", + "Careless", + "Avaricious", + "Mischievous", + "Soft", + "Unrivaled", + "Swole", + "Hip", + "Brand-New", + "Full-Throttle", + "Peppy", + "Law-Breaking", + "At-Work", + "The First", + "Promising", + "Ink-Sloshing", + "Wave-Riding", + "Swanky", + "Fresh-Catch", + "Scientific", + "Orderly", + "The Lone", + "Sappy", + "Dodge-Rolling", + "Notable", + "Explosive", + "Shy", + "Calamari", + "Fishy", + "Puffed-Up", + "Fuzzy", + "The Great", + "Birthday-Celebratin'", + "Whimsical", + "Dangerous", + "Legit", + "Dandy", + "Ultra", + "Floaty", + "Indoor", + "Ink-Covered", + "Ink-Dripping", + "Fresh", + "Wicked-Fresh", + "Stale", + "A Simple", + "8-Tentacled", + "10-Tentacled", + "Midwinter", + "Mind-Blowing", + "Lifelong", + "Top-Ranked", + "Actual", + "Growing", + "Studious", + "Overpowering", + "Spineless", + "Headstrong", + "Leaping", + "Running", + "Protective", + "Attacking", + "Watchful", + "Cooperative", + "Hidden", + "Submersible", + "Brute", + "Stealthy", + "Everyday", + "Gaming", + "Logical", + "Reliable", + "Gentle", + "Confident", + "Serious", + "Mild-Mannered", + "Cheerful", + "High-End", + "Chatty", + "Cash-Stacking", + "Absentminded", + "Beloved", + "Windy", + "Watery", + "Sneaky", + "Fiery", + "Incandescent", + "Young", + "Organized", + "Perfect", + "Mysterious", + "Central", + "Fierce", + "Ambiguous", + "Instantaneous", + "Exemplary", + "Tear-Stained", + "Traveling", + "Next Year's", + "Twilight", + "The One and Only", + "Hibernating", + "Sharp-Eyed", + "Lost", + "Procrastinating", + "Honorary", + "Tasteless", + "Merciless", + "Infinite", + "Sleepy", + "Time-Traveling", + "Suave", + "Howling", + "Phantasmagoric", + "Celestial", + "Aged", + "Lethal", + "Secret", + "Esoteric", + "Exhausted", + "Better-Than-You", + "All-Purpose", + "Gleaming", + "Midday", + "Unnoticed", + "Unprecedented", + "Tenacious", + "Scorching", + "Daytime", + "Meaty", + "Enigmatic", + "Unflappable", + "Savvy", + "Fleeing", + "Drained", + "Reincarnated", + "Sun-Dried", + "AKA", + "Superhuman", + "Subterranean", + "Massive", + "Terrestrial", + "Important", + "Jumbo", + "Oceanic", + "Big", + "Easygoing", + "Ancient", + "Multiplying", + "Fully Automatic", + "Glimmering", + "Seasick", + "Unblemished", + "Fightin'", + "Front-Line", + "Endangered", + "Persistent", + "Sacred", + "Local", + "Turn-of-the-Century", + "This Century's Best", + "Aquarium-Bound", + "Fan-Favorite", + "Rising", + "Spicy", + "Lightning-Fast", + "Awe-Inspiring", + "Midnight", + "Stark-White", + "Deep-Red", + "Bright-Blue", + "Pitch-Black", + "Woodsy", + "Next-Generation", + "Novice", + "Sensational", + "Dead-Tired", + "Dazzling", + "Tagalong", + "Little", + "Third-Eye-Opened", + "Early-Summer", + "Alabaster", + "Pure", + "Grown-Up", + "Partying", + "Dapper", + "Evil", + "Jet-Black", + "Vindictive", + "Self-Proclaimed", + "Selfie-Snapping", + "Eternity's", + "Self-Identified", + "Childish", + "Job-Lovin'", + "Late-Summer", + "Cutting-Edge", + "Mountain-Dwelling", + "Picture-Taking", + "Yesterday's", + "Graceful", + "First-Wave", + "All-New", + "The Very First", + "The Final", + "This Year's", + "Limited-Edition", + "Ever-Present", + "Booming", + "Crimson", + "Steady", + "Nosy", + "Enlightened", + "Aloof", + "Hearty", + "Artsy", + "Flying", + "Die-Hard", + "Fortunate", + "Last Year's", + "The True Ultimate", + "Impish", + "Shining", + "Forgetful", + "Heartfelt", + "Unarmed", + "Sweet", + "Sentimental", + "Sandy", + "Overheated", + "Catch-of-the-Day", + "Pool-Ready", + "Anachronistic", + "Supersonic", + "Lovely", + "Movie-Lovin'", + "Shadowy", + "Fated", + "Cosmic", + "Zipped-Lips", + "Gone-Fishin'", + "24-Hour", + "Mostly", + "Otherworldly", + "Junior-Grade", + "Wild", + "A Real", + "Lucky", + "Seasoned", + "Soft-and-Fluffy", + "100%", + "Veggie-Lovin'", + "Fluffy", + "Modern", + "Lost-and-Found", + "Won't-Lose", + "Auspicious", + "Innocent", + "Miraculous", + "Poser", + "Comic-Lovin'", + "Vulnerable", + "Light", + "Bomb-tastic", + "Fluttering", + "Buoyant", + "Wibbly-Wobbly", + "Fruit-Lovin'", + "Dark", + "Fragrant", + "Fashionable", + "Big-Time", + "Drenched", + "Bearded", + "Frosty", + "Incredible", + "Faraway", + "Flashy", + "Fame-Seeking", + "Bursting", + "Designer-Label", + "Swoon-Worthy", + "Strict", + "Slippery", + "Painted", + "Climactic", + "Nameless", + "Nice", + "Muddy", + "Neighborly", + "Tidy", + "Barbed", + "Pointy", + "Big Honkin'", + "Smooth", + "Iced", + "Occasional", + "Just Another", + "Essential", + "Diving", + "Dynamite", + "Overall", + "Bargain-Binned", + "Slim", + "Out-of-Options", + "Steampunky", + "Numb", + "Hiking", + "Wandering", + "Unyielding", + "Economical", + "Gothic", + "Covert", + "High-Performing", + "Praiseworthy", + "Arcade-Hopping", + "Pretty Good", + "The GOAT", + "Glistening", + "Sparkling", + "Gigantified", + "Camera-Facing", + "Clockwork", + "Solid", + "Fully", + "Unperceived", + "Carnivorous", + "Homebound", + "Fish-Lovin'", + "Sketching", + "Precocious", + "Easy-Peasy", + "Made-to-Order", + "Elegant", + "Water-Breathing", + "The Eternal", + "Instant", + "The Usual", + "Ready-to-Go", + "Lively", + "Unstoppable", + "Underground", + "The Real Ultimate", + "Overflowing", + "Toon-Lovin'", + "Splendid", + "Warm-and-Cozy", + "Amped-Up", + "Outdoorsy", + "Tech-Minded", + "Lovable", + "amiibo-Collecting", + "5-Year-Planning", + "Decade-Experienced", + "Crab-N-Go", + "Summer-Fun", + "Winter-Ready", + "Not-Streaming", + "Daylit", + "Dawning", + "Realistic", + "Lightly Snacking", + "Late-Night", + "Comfy-Cozy", + "Fearful", + "Winter-Breaking", + "So-Called", + "Intuitive", + "A Complex", + "The Original", + "Second-Generation", + "The Best", + "Roaming", + "DJ", + "Badlands", + "Snowy", + "Seaside", + "Big-Sky", + "Dry", + "Raw", + "Awesome", + "SUPERFRESH", + "Splatoon", + "Avant-Garde", + "Punk", + "Mod", + "Goth", + "Rockabilly", + "Casual", + "Sporty", + "Breakin'", + "Glam", + "Vintage", + "Bohemian", + "Grunge", + "Hip-Hop", + "Thunderous", + "Turbo", + "Wizened", + "Today's", + "Virtual", + "Half", + "Microscopic", + "Swift", + "Everyone's", + "Carefree", + "New", + "High-Earning", + "Pool-Frequenting", + "Revived", + "Preposterous", + "Mighty", + "Chill", + "Gossip-Loving", + "Social", + "Crafty", + "Hopeful", + "Sealed", + "Theoretically Possible", + "Rookie", + "Sleeved", + "World's First", + "Nature-Loving", + "Inflexible", + "Prized", + "Faux", + "Reasonable", + "Unfettered", + "Unthinkable", + "Phantom", + "Cherished", + "Gold", + "Silver", + "Undefeated", + "New Year's", + "This Year's Final", + "Newly Released", + "Tonight's", + "Reclusive", + "Stewed", + "Cyber-Brainiac", + "Magical", + "Of-the-Moment", + "Forevermore", + "Anticipated", + "Automated", + "Drab", + "Reflective", + "Hoary", + "Parallel World's", + "Power-Concealing", + "Nourishing", + "Highly Nutritious", + "Ascending", + "Sprightly", + "Always-in-Our-Hearts", + "Sanguine", + "Daybreaking", + "Repeating", + "Endlessly Ringing", + "Listless", + "In-Season", + "Rampaging", + "Scintillating", + "Abyssal", + "Originally from", + "Unknowing", + "Currently Obsessed with", + "Nonstop", + "Vivacious", + "Aggressive", + "Immediate", + "Natural", + "High-Calorie", + "Low-Calorie", + "Recently", + "Esteemed", + "Treasured", + "Existing", + "Nonexistent", + "On-the-Job", + "Choosy", + "Delicate", + "Haphazard", + "Golden Egg–Like", + "Ice-Cold", + "Low-Battery", + "Fully Charged", + "Noontime", + "Break-Time", + "Unavailable", + "Protecting", + "Long-Awaited", + "Now-Stronger", + "Breath-Holding", + "Genuine", + "Nimble", + "Movable", + "At-Home", + "Hometown", + "Shooter-Using", + "Roller-Using", + "Charger-Using", + "Slosher-Using", + "Splatling-Using", + "Dualie-Using", + "Brella-Using", + "Blaster-Using", + "Brush-Using", + "Stringer-Using", + "Splatana-Using", + "Replete with", + "Watered", + "Triumphant", + "Tasteful", + "Outstanding", + "Stylish", + "Three-Star", + "Scheduled", + "Trailblazing", + "Satiating", + "Newish", + "Same-as-Ever", + "Multifaceted", + "Orthodox", + "Regal", + "Classic", + "Old-School", + "Simplistic", + "Advanced", + "Prototypical", + "Sincere", + "Flexible", + "Creative", + "Classy", + "Slender", + "Genteel", + "Shrouded-in-Mystery", + "Novel", + "Brisk", + "Undying", + "Powerful", + "Unwavering", + "Heroic", + "Swaggering", + "Astute", + "Pushy", + "Resilient", + "Burly", + "Resourceful", + "Lax", + "Daredevilish", + "Magnetic", + "Dynamic", + "Unique", + "Keen-and-Nimble", + "Cordial", + "Dignified", + "Flowy", + "Cryptic", + "High-Spirited", + "Mind-Changing", + "Chic", + "Witty", + "Cheery", + "Vivid", + "Rare", + "Closed-Off", + "Surreal", + "Methodical", + "Gorgeous", + "Cute", + "Awkward", + "Deep", + "Unadorned", + "Cushy", + "Gallant", + "Adept", + "Invigorating", + "Veiled", + "Discreet", + "Frank", + "Rational", + "Charming", + "Quiet", + "Wise", + "Introverted", + "Abstract", + "Sharp", + "Gutsy", + "Authentic", + "Learned", + "Experienced", + "Gifted", + "Ingenious", + "Healthy", + "Honest", + "Just-Right", + "Blustery", + "Funky", + "Exciting", + "Exuberant", + "Well-Mannered", + "Dutiful", + "Modest", + "Prestigious", + "Affable", + "Sociable", + "Good-Humored", + "Urbane", + "Meek", + "Well-Rounded", + "Well-Behaved", + "Upstanding", + "Big-Hearted", + "Influential", + "Mold-Breaking", + "Ghastly", + "Academic", + "Plucky", + "Cheeky", + "Glorious", + "Maniacal", + "Magnificent", + "Energetic", + "Misplaced", + "De Facto", + "333x", + "Ageless", + "Apparent", + "Ostensible", + "Heart-Thumping", + "Starchy", + "Barely", + "Narrowly", + "Amusing", + "Excited", + "Swishy", + "The Genuine", + "Unnamed", + "Glory-Hogging", + "Prickly", + "Lackadaisical", + "Surviving", + "Eccentric", + "Idiosyncratic", + "Rule-Breaking", + "Snazzy", + "Zesty", + "Most Popular", + "On-the-Rise", + "Skyrocketing", + "World-Wise", + "Assisting", + "Covering", + "Bourgeois", + "Clean", + "Almighty", + "Robust", + "Enlivened", + "Proactive", + "Unruly", + "Fully Committed", + "Delightful", + "Flower-Shaped", + "Extraordinary", + "Original", + "Top-Notch", + "Clandestine", + "Painful", + "Topical", + "Lavish", + "Thrilling", + "Deluxe", + "Triple", + "Twin", + "Double", + "Individual", + "Comical", + "Imperfect", + "Uncommon", + "Peerless", + "Vertebrate", + "Expert", + "Giant", + "Stalwart", + "Exquisite", + "Acrobatic", + "Captivated", + "Fledgling", + "Relaxed", + "Playful", + "True", + "Candid", + "Straightforward", + "Unalloyed", + "Magnanimous", + "Stoic", + "Angelic", + "Single-Minded", + "Bland", + "Rich", + "Earnest", + "Forward-Plunging", + "Total", + "Passionate", + "Inexorable", + "Plain", + "Thoughtful", + "Frizzy", + "Somehow", + "Upright", + "Peaceful", + "Ephemeral", + "Bright", + "Laid-Back", + "Dried-Out", + "Metallic", + "Common", + "Nonchalant", + "Stubborn", + "Custom", + "Self-Centered", + "Determined", + "Indomitable", + "Unswerving", + "Steadfast", + "All-Out", + "Imperative", + "Undiscouraged", + "Enduring", + "Persisting", + "Completing", + "Piercing", + "Hyped", + "Climbing", + "Proper", + "Noteworthy", + "Relieved", + "Brazen", + "Unaffected", + "Exceptional", + "Challenging", + "Intellectual", + "Fair", + "Discerning", + "First-Rate", + "Strong-Willed", + "Formidable", + "Direct", + "Ambitious", + "Sturdy", + "Dogged", + "Dependable", + "Meticulous", + "Enterprising", + "Courageous", + "Resolved", + "Zealous", + "Insider", + "Skilled", + "Enthusiastic", + "Wonderful", + "Heavy-Duty", + "Struggling", + "Ultimate", + "Convenient", + "Restless", + "Composed", + "Comprehensible", + "Abandoned", + "Last", + "Supernatural", + "Clamorous", + "Dense", + "Airy", + "My Very Own", + "Improved", + "Blue", + "Ridiculous", + "Very", + "Really", + "Dauntless", + "Perpetual", + "Recently Graduated", + "Retirement-Age", + "Commencing", + "Genius", + "Praying", + "Sometime", + "Occasionally", + "Overdone", + "Lunchtime", + "Speciality", + "Phoenix-Like", + "Slacking", + "Grateful", + "Absolute", + "Blatant", + "Wistful", + "Airborne", + "Temporary", + "Conspicuous", + "Mesmerizing", + "Bronzed", + "Normal", + "Lightly Seasoned", + "Pampered", + "Simulated", + "Splatfest's", + "Splatfest Battle's", + "Rugged", + "Grand Festival", + "Monochrome", + "Long-Sleeved", + "Collab-Loving", + "Swaggy", + "Refreshing", + "Simmering", + "Positive", + "Dim", + "Stable", + "Merry", + "Ebullient", + "Tailwind-Riding", + "Peak-Condition", + "Thank You for", + "Super-Duper Lucky", + "Super-Rare", + "One-in-a-Million", + "Ace", + "Astonishing", + "Adored", + "Charmed", + "Shell-Out", + "Splatlandian", + "Indian Ocean", + "North Atlantic", + "North Pacific", + "Antarctic Ocean", + "Arctic Ocean", + "South Atlantic", + "South Pacific", + "The Shoal's Own", + "Official-Grizzco", + "The Crater's", + "The City's Own", + "Lobby-Bound", + "Third-Gen", + "Chaotic", + "Midsummer", + "Alternan", + "Reminiscent", + "Memversal", + "Side Orderly", + "Swarming", + "Marching", + "Battering", + "Jelletonal", + "Drizzling", + "Panicking", + "Whirling", + "Springing", + "Gushing", + "Spawning", + "Homing", + "Towering", + "Layered-Rotator", + "Elusive-Bounder", + "Intensifying-Harmony", + "Fun", + "The Strongest", + "Crusty", + "Saddle-Straddlin'", + "Streamlined", + "Wandercrust", + "Tender", + "Moving", +]; + +export const SUBTITLES_PLURALIZED = [ + "Sploosh-o-matic Users", + "Splattershot Jr. Users", + "Splash-o-matic Users", + "Aerospray Users", + "Splattershot Users", + ".52 Gal Users", + "N-ZAP Users", + "Splattershot Pro Users", + ".96 Gal Users", + "Jet Squelcher Users", + "Luna Blaster Users", + "Blaster Users", + "Range Blaster Users", + "Clash Blaster Users", + "Rapid Blaster Users", + "Rapid Blaster Pro Users", + "L-3 Nozzlenose Users", + "H-3 Nozzlenose Users", + "Squeezer Users", + "Carbon Roller Users", + "Splat Roller Users", + "Dynamo Roller Users", + "Flingza Roller Users", + "Inkbrush Users", + "Octobrush Users", + "Squiffer Users", + "Splat Charger Users", + "E-liter 4K Users", + "Bamboozler 14 Users", + "Goo Tuber Users", + "Slosher Users", + "Tri-Slosher Users", + "Sloshing Machine Users", + "Bloblobber Users", + "Explosher Users", + "Mini Splatling Users", + "Heavy Splatling Users", + "Hydra Splatling Users", + "Ballpoint Splatling Users", + "Nautilus Users", + "Dapple Dualies Users", + "Splat Dualies Users", + "Glooga Dualies Users", + "Dualie Squelchers Users", + "Tetra Dualies Users", + "Splat Brella Users", + "Tenta Brella Users", + "Undercover Brella Users", + "Tri-Stringer Users", + "Splatana Stamper Users", + "REEF-LUX 450 Users", + "Splatana Wiper Users", + "Splatterscope Users", + "E-liter 4K Scope Users", + "Splattershot Nova Users", + "Big Swig Roller Users", + "Snipewriter 5H Users", + "S-BLAST '92 Users", + "Painbrush Users", + "Dread Wringer Users", + "Heavy Edit Splatling Users", + "Douser Dualies FF Users", + "Recycled Brella 24 Mk I Users", + "Wellstring V Users", + "Decavitator Users", + "Splat Bomb Users", + "Suction Bomb Users", + "Burst Bomb Users", + "Curling Bomb Users", + "Autobomb Users", + "Ink Mine Users", + "Sprinkler Users", + "Point Sensor Users", + "Toxic Mist Users", + "Splash Wall Users", + "Squid Beakon Users", + "Fizzy Bomb Users", + "Torpedo Users", + "Angle Shooter Users", + "Ink Storm Users", + "Trizooka Users", + "Ultra Stamp Users", + "Big Bubbler Users", + "Inkjet Users", + "Wave Breaker Users", + "Zipcaster Users", + "Reefslider Users", + "Crab Tank Users", + "Booyah Bomb Users", + "Ink Vac Users", + "Tenta Missiles Users", + "Killer Wail 5.1 Users", + "Triple Inkstrike Users", + "Tacticooler Users", + "Super Chump Users", + "Kraken Royale Users", + "Triple Splashdown Users", + "Splattercolor Screen Users", + "SquidForce Models", + "Zink Models", + "Krak-On Models", + "Rockenberg Models", + "Zekko Models", + "Forge Models", + "Firefin Models", + "Skalop Models", + "Splash Mob Models", + "Inkline Models", + "Tentatek Models", + "Takoroka Models", + "Annaki Models", + "Enperry Models", + "Toni Kensa Models", + "Z+F Models", + "Barazushi Models", + "Emberz Models", + "Cuttlegear Models", + "Squid Squad Fans", + "Chirpy Chips Fans", + "Hightide Era Fans", + "Wet Floor Fans", + "Bottom Feeders Fans", + "Ink Theory Fans", + "SashiMori Fans", + "Diss-Pair Fans", + "ω-3 Fans", + "Turquoise October Fans", + "Dedf1sh Fans", + "C-Side Superfans", + "Front Roe Fans", + "Damp Socks Fans", + "Yoko & the Gold Bazookas Fans", + "H2Whoa Fans", + "Riot Act Fans", + "Now or Never Seven Fans", + "Hippies", + "Barbers", + "Double-Buns", + "Wave Riders", + "Pigtail Pros", + "Headbangers", + "Topknot Knights", + "Sharp Dressers", + "Buzz-Cuts", + "Ponytails", + "Spiked Hairs", + "Bowl Cuts", + "Braid Bearers", + "Classics", + "Wet Hairs", + "Cornrowses", + "Curly Hairs", + "Punks", + "Afros", + "Big Curlses", + "Fades", + "Wanderers", + "Greasers", + "Hipster s", + "Callie Fans", + "Naut Couture Regulars", + "Marina Fans", + "Frye Fans", + "Crush Station Regulars", + "Hotlantis Regulars", + "Squid Sisters Fans", + "Deep Cut Fans", + "Off the Hook Fans", + "Pearl Fans", + "Shiver Fans", + "Ammo Knights Regulars", + "Man-o'-Wardrobe Regulars", + "Marie Fans", + "Big Man Fans", + "Clam Blitz Enthusiasts", + "Splat Zones Enthusiasts", + "Rainmaker Enthusiasts", + "Tower Control Enthusiasts", + "Salmon Runners", + "Turf Warriors", + "Story Mode Heros", + "Clam Blitz Legends", + "Splat Zones Legends", + "Tower Control Legends", + "Turf War Legends", + "Rainmaker Legends", + "Salmon Run Legends", + "Story Mode Legends", + "Inklings", + "Octolings", + "Tee Models", + "Jacket Models", + "Shirt Models", + "Sneaker Models", + "Knit Models", + "Trendsetters", + "Hat Models", + "Glasses Models", + "Boot Models", + "Card Collectors", + "Item Collectors", + "Fashionistas", + "Locker Rock Stars", + "Squid Surgers", + "Squid Rollers", + "All-Arounders", + "Jumpers", + "Dodge Rollers", + "Slow Starters", + "Splat Bombers", + "Guardians", + "Raiders", + "Manta Rays", + "Octarians", + "Bears", + "Jellyfishes", + "Humans", + "Sea Creatures", + "Mammals", + "Artists", + "Pop Stars", + "Clock Punchers", + "Eel Tamers", + "Shark Tamers", + "Clerks", + "Office Drones", + "Ringleaders", + "Champs", + "Bass Droppers", + "Princes", + "Brothers", + "City Boys", + "Grooms", + "Kings", + "Bros", + "Oden Bowls", + "Party Lovers", + "Surfers", + "Festivalgoers", + "Flowers", + "Heat Haters", + "Snowpersons", + "Firecrackers", + "Eyes of the Storm", + "Fish", + "Deity of Destructions", + "Dogs", + "Dragons", + "Cats", + "Dinosaurs", + "Cephalopods", + "Bivalves", + "Dolphins", + "Bookworms", + "CEOs", + "Assistants", + "Athletes", + "Agents", + "Risk-Takers", + "Coaches", + "Coordinators", + "Comedians", + "Scientists", + "Samurais", + "Spies", + "Dancers", + "Designers", + "Ninjas", + "Part-Timers", + "Pilots", + "Hunters", + "Planners", + "Programmers", + "Producers", + "Painters", + "Managers", + "Models", + "Writers", + "Executives", + "Astronauts", + "Singers", + "Presidents", + "Vikings", + "Students", + "Directors", + "Leads", + "Anglers", + "Assistant Managers", + "Security Guards", + "Researchers", + "Authors", + "Workers", + "Butlers", + "VPs", + "Board Members", + "Gofers", + "Artisans", + "Teachers", + "Village Leaders", + "Head Honchos", + "Detectives", + "Mountaineers", + "Professors", + "Secretaries", + "Beauticians", + "Middle Managers", + "Vice Presidents", + "Adventurers", + "Comic Artists", + "Heros", + "Chefs", + "Layabouts", + "Big Mouths", + "Chow Hounds", + "Dynamos", + "Hotheads", + "Octoling Soldiers", + "Power Eggs", + "Villains", + "Fams", + "Turtles", + "Forms", + "Knights", + "Eggs", + "Entertainers", + "Comrades", + "Superheros", + "Jokesters", + "Magicians", + "Brainiacs", + "Fish Balls", + "Materials", + "Sushis", + "Callers", + "Seafood", + "Mincemeats", + "Squid Jerkies", + "Booyahs", + "Vitamins", + "Weapons", + "Dried Fish", + "Muscles", + "Illusions", + "Shields", + "Leaders", + "Viewers", + "Systems", + "Tournament Competitors", + "Agent 1", + "Agent 2", + "Agent 4", + "Agent 8", + "AIs", + "Object of Affections", + "Such and Suches", + "Outlaws", + "Heels", + "Tabletop Gamers", + "Androids", + "Legends", + "Squid Inks", + "Squid Lives", + "Squid Rings", + "Squid, Inc.s", + "Inks", + "Influencers", + "Moray Eels", + "Elegances", + "Big Deals", + "Big Bosses", + "Otakus", + "Dance Masters", + "Freebies", + "Kids", + "Pack Leaders", + "Photographers", + "Fried Chickens", + "Extroverts", + "Carpaccios", + "Synth Players", + "Guitarists", + "Campers", + "Cupids", + "Crabs", + "Creators", + "Grooves", + "Gamers", + "Ultimate Weapons", + "Survivalists", + "Iconoclasts", + "Supporters", + "Sharks", + "Salmon Lives", + "Swimmers", + "Styles", + "Strikers", + "Streamers", + "Gelatins", + "Celebs", + "Taste Testers", + "Title Holders", + "Octopus Inks", + "Octo Lives", + "Tuna Tatakis", + "Challengers", + "Butterfingerses", + "Drivers", + "Drummers", + "Tornados", + "Trainers", + "Party Animals", + "Polyglots", + "Rebels", + "Workhorses", + "Introverts", + "Clubgoers", + "Stand-Up Comics", + "Fighters", + "Mirages", + "Planktons", + "Brand Loyalists", + "Freelancers", + "Snowstorms", + "Friends", + "Headliners", + "Seniors", + "Vocalists", + "Board Gamers", + "Bosses", + "Bodyguards", + "Mermaids", + "Masks", + "Superstars", + "Maniacs", + "Mysteries", + "Bifocal Wearers", + "Therapists", + "Members", + "Lifestyles", + "Rappers", + "Remote Workers", + "Rookies", + "Robots", + "Enthusiasts", + "Ordinary People", + "Lone Wolves", + "Paupers", + "Prodigies", + "Monarches", + "Hot-Spring Enjoyers", + "Fish Finders", + "Saltwater Fish", + "Umibozus", + "Cold Chills", + "Sightseers", + "Prime Cuts", + "Moods", + "Memories", + "Demons", + "Giants", + "Fish Prints", + "Mirrors", + "Avocado Rolls", + "Moons", + "Fists", + "Cavemans", + "Experts", + "Novices", + "Foremans", + "Ruffians", + "Comic Reliefs", + "Advisers", + "Sashimis", + "Legendary Warriors", + "Underlings", + "Senseis", + "Free Spirits", + "Sidekicks", + "Fledglings", + "Employees", + "Greases", + "Beginners", + "Imps", + "Higher-Ups", + "Trainees", + "Rookie of the Years", + "Freshmans", + "Truth Tellers", + "Kingpins", + "Lives of the Party", + "Wallflowers", + "Lives Itself", + "Humanities", + "Supervisors", + "Fillets", + "Inkantations", + "Penny-Pinchers", + "Mountaintop Gurus", + "Mentors", + "Warriors", + "Berserkers", + "Blade of Grasses", + "Entities", + "Suns", + "Representatives", + "Cabinet Members", + "Freshwater Fish", + "Group Members", + "Style Icons", + "Bugs", + "Contenders", + "Birds", + "Stans", + "Fisherfolks", + "Apprentices", + "Targets", + "Maestros", + "Angels", + "Island Dwellers", + "Tofus", + "Foreheads", + "Plumbers", + "Content Creators", + "Merpersons", + "Sidesteppers", + "Flotsams", + "Subordinates", + "Walls", + "Predators", + "Equations", + "Archdemons", + "Pros", + "Lives", + "Fuzzballs", + "Actors", + "Ghosts", + "Atlanteans", + "Teardrops", + "Ronins", + "Rice Bowls", + "Outfits", + "Palettes", + "Snackses", + "Followers", + "Marinades", + "Switch-Hitters", + "Stir-Fries", + "Catfish", + "Grand Masters", + "Cubs", + "DJs", + "Paradigms", + "Geeks", + "Heads", + "Fits", + "B-Boys", + "Skaters", + "Dukes", + "Celebrities", + "Mariners", + "Fashion Designers", + "Commanders", + "Store Managers", + "Patrons", + "Sparklers", + "Fantasy Lovers", + "Horrorboroses", + "Cohozunas", + "Floats", + "Nuances", + "Aliens", + "Newcomers", + "Hopes", + "MCs", + "Special Attacks", + "Special Spaces", + "Rankers", + "Aces in the Hole", + "Decks", + "Hole Cardses", + "Test Takers", + "Personnels", + "Philosophers", + "One-Hit Wonders", + "Organizations", + "Sleeve Collectors", + "Rock Climbers", + "Bungee Jumpers", + "Soliloquys", + "Sea-Cucumber Phones", + "Ripples", + "Salts", + "Peppers", + "Mayonnaises", + "Seasonings", + "Values", + "Time Killers", + "Clumps", + "Repeat Customers", + "Mushrooms", + "Stars", + "Talented People", + "Superpersons", + "Seaweeds", + "Kelps", + "Marine Snows", + "Bento Boxes", + "Fish Eggses", + "Stews", + "Simmered Foods", + "Prix Fixes", + "Curries", + "Aficionados", + "Informations", + "Heart Valves", + "Hearts", + "Rocks", + "Jazzes", + "Solos", + "Soup Stocks", + "Soups", + "Salads", + "Champions", + "Rice Balls", + "Paradoxes", + "Dressings", + "Sauces", + "Spices", + "Specialists", + "Specialty Shops", + "Bandleaders", + "Runners", + "Speedruns", + "Jukeboxes", + "Demigods", + "Laughs", + "Theme Parks", + "Hairstyles", + "Encores", + "Settlers", + "Storms", + "Breezes", + "Observers", + "Right Arms", + "Sea Urchins", + "Pianos", + "Guitars", + "Keyboards", + "Drums", + "Castanets", + "Recorders", + "Cymbalses", + "Musical Instruments", + "Prime Ministers", + "Project Managers", + "Physicians", + "Lawyers", + "Accountants", + "Bodybuilders", + "Lads", + "Bases", + "Sambas", + "Machos", + "Cool Guys", + "Thespians", + "Hard Workers", + "Ship Captains", + "Chaperones", + "Time Travelers", + "Puddings", + "Counterfeits", + "Alter Egos", + "Grillers", + "Smallfries", + "Cohocks", + "Snatchers", + "Steelheads", + "Steel Eels", + "Scrappers", + "Stingers", + "Mawses", + "Drizzlers", + "Flyfishes", + "Fish Sticks", + "Flipper-Floppers", + "Slammin' Lids", + "Big Shots", + "Goldies", + "Motherships", + "Mudmouths", + "Glowflies", + "Ranks", + "Romanticists", + "Steel Bodies", + "Flavors", + "Elites", + "Determined Spirits", + "Person of Influences", + "Trailblazers", + "Casts", + "Aces", + "Number Ones", + "Foremost Figures", + "Mainstays", + "Emperors", + "Paragons", + "Intermediaries", + "Prototypes", + "Schemers", + "Brave Warriors", + "Person of Cultures", + "Honor Students", + "Brains", + "Trappers", + "Intellectuals", + "Impractical Theories", + "Dislikes", + "Meisters", + "Governors", + "Office Chiefs", + "Caretakers", + "Bank Presidents", + "Harlequins", + "Focal Points", + "Event Organizers", + "VIPs", + "Performers", + "Woodcutters", + "Sous-Chefs", + "Engineers", + "Contractors", + "Prosperous Merchants", + "Referees", + "Manila Clams", + "Concierges", + "Tentacles", + "Nobles", + "Temps", + "Pranksters", + "Great Regents", + "Main Characters", + "Leading Actors", + "Dark Horses", + "Key Figures", + "Masterminds", + "Tacticians", + "Vagabonds", + "Voyagers", + "Wandering Hermits", + "Nomads", + "Bounty Hunters", + "Timeline Jumpers", + "Generations", + "Elders", + "Captains", + "Guests", + "String Pullers", + "Galleries", + "Big Names", + "Regular Customers", + "Partners", + "Cyborgs", + "Successes", + "Shadow Commanders", + "Helpers", + "Backers", + "Millionaires", + "Custodians", + "Lieutenants", + "Origins", + "Czars", + "Cheer Teams", + "Staffs", + "Factotums", + "Energies", + "Engines", + "Consultants", + "Tutors", + "Puppet Masters", + "Buddies", + "Critics", + "Luck Pushers", + "Teams", + "Buffoons", + "Players", + "TV Personalities", + "Announcers", + "Geniuses", + "Golfers", + "Batters", + "Pitchers", + "Catchers", + "Benchwarmers", + "Virtuosos", + "Conductors", + "Musical Performers", + "Lyricists", + "Pianists", + "Ingenues", + "Narrators", + "Pro Wrestlers", + "Sprinters", + "Boxers", + "Extras", + "Cinematographers", + "Stunt Performers", + "Descendants", + "Offsprings", + "Sponsors", + "Suggestions", + "Facilitators", + "Showstoppers", + "Possessors", + "Housekeepers", + "Traders", + "Fortune Tellers", + "Globe-Trotters", + "Pinch Hitters", + "Leadoff Hitters", + "Musketeers", + "Crowds", + "Circles", + "Flocks", + "Cooperators", + "Units", + "Armies", + "Coteries", + "Quartets", + "Choruses", + "Podium Finishers", + "Guilds", + "Groups", + "Horses", + "Rabbits", + "Pandas", + "Koalas", + "Squirrels", + "Sea Otters", + "Elephants", + "Giraffes", + "Whales", + "Tigers", + "Lions", + "Hamsters", + "Deer", + "Bookkeepers", + "Idea People", + "Rock 'n' Rollers", + "Controllers", + "Storytellers", + "Illustrators", + "Finger Painters", + "Scriveners", + "Founders", + "Families", + "Craftsmans", + "Machines", + "Navigators", + "Inventors", + "Parties", + "Playmakers", + "Young Leaders", + "Mascots", + "Fairies", + "Joke Tellers", + "Voice Actors", + "Learners", + "Tenderfoots", + "Heads of the Class", + "University Students", + "Architects", + "Costumiers", + "Ferocious Fighters", + "Knowledgeable People", + "Lofty Dreams", + "Wizes", + "Prominent Figures", + "Grapplers", + "Natural Enemies", + "Horseshoe Crabs", + "Pond Snails", + "Nautiluses", + "Coconut Crabs", + "Sea Anemones", + "Goldfish", + "Horse Mackerels", + "Devil Rays", + "Sea Slugs", + "Shrimp", + "Spider Crabs", + "Rivals", + "Weaknesses", + "Types", + "Sweethearts", + "Imitations", + "Processes", + "Gatherings", + "Shoals", + "Crews", + "Beasts", + "Rulers", + "Fossils", + "Wardens", + "Collectors", + "Crushes", + "Souls", + "State of Minds", + "Marathons", + "Good People", + "Clients", + "Dolls", + "Noble Scions", + "Protagonists", + "Symbols", + "Authorities", + "Contributors", + "Cartoon Characters", + "Plushies", + "Spirits", + "Thoroughbreds", + "Person of Notes", + "Professionals", + "Gourmets", + "Sumo Wrestlers", + "Family Trees", + "Soldiers", + "Order of Knightses", + "Victors", + "Villagers", + "School Principals", + "Protectors", + "Explorers", + "Seekers", + "Pugilists", + "Paladins", + "Door Guards", + "Rangers", + "Monks", + "Sages", + "Doctors", + "Blacksmiths", + "Postdocs", + "Dignitaries", + "Artistes", + "Interns", + "Alchemists", + "Security Details", + "Ores", + "Guides", + "Chauffeurs", + "Bloggers", + "Operators", + "Librarians", + "Objet d'Arts", + "Successors", + "Owners", + "Proxies", + "Pupils", + "Disciples", + "Scholars", + "Graduates", + "Collegians", + "PhD Candidates", + "Ketchups", + "Bamboo Shoots", + "Pops", + "Psychics", + "Arbiters", + "Litterateurs", + "Megalodontias", + "Recollections", + "Stylists", + "Entrepreneurs", + "Musicians", + "Party Stars", + "Mercenaries", + "Alliances", + "Bards", + "Otherworldly Beings", + "Chaoses", + "Noobs", + "Vanguards", + "Luminaries", + "Playings", + "Lucky Ducks", + "Protected Specieses", + "Chosen Ones", + "Gadabouts", + "Point Guards", + "Optimists", + "Daredevils", + "Prizewinners", + "Youths", + "Squid", + "Octopuses", + "Salmon", + "Boys", + "Agent 3", + "Fathers", + "Dads", + "Sons", + "Big Brothers", + "Little Brothers", + "Grandpas", + "Grandchilds", + "Uncles", + "Youngsters", + "Pets", + "World Champs", + "Mediators", + "Beta Testers", + "Revelers", + "Languendos", + "Andantes", + "Lentos", + "Portals", + "Capricciosos", + "Alla Mambos", + "Accelerandos", + "Spiccatos", + "Trionfales", + "Accordos", + "Arpeggios", + "Nobilmentes", + "Asynchronous Rondos", + "Pinging Marciales", + "Parallel Canons", + "Tableturf Battlers", + "Card Gamers", + "Travelers", + "Cyclists", + "Tempuras", + "Boosters", + "Resiliences", + "Finales", +]; diff --git a/app/utils/team-name.ts b/app/utils/team-name.ts new file mode 100644 index 000000000..6428dfd61 --- /dev/null +++ b/app/utils/team-name.ts @@ -0,0 +1,12 @@ +import { ADJECTIVES, SUBTITLES_PLURALIZED } from "./team-name-data"; + +/** Generates a random team name by combining a Splatoon 3 title adjective with a pluralized subtitle e.g. "Prestigious Heat Haters" */ +export function randomTeamName() { + const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]; + const subtitle = + SUBTITLES_PLURALIZED[ + Math.floor(Math.random() * SUBTITLES_PLURALIZED.length) + ]; + + return `${adjective} ${subtitle}`; +} diff --git a/db-test.sqlite3 b/db-test.sqlite3 index 87474164b4c63d53094d7e9eed90d3a07b370725..24c6c020967d9522c64e50d29408a9885915efd5 100644 GIT binary patch delta 5834 zcmeHLeN0=|6@S-$&kuib-vf+oY~u$6i~|op``LhL$H<~2Z5L&vO;$npr~?jtU@*bX zN4rfSbcL!_^D(!*(<*HZXtZmRDk@o?BCXBGq(zmMG})HGJ{y*)Q>9H?J57^Vn(ktf z6kp>`llCXb*OGtSbI$Lab3e|xqoWUvj&3~S;4e}Xuu=E_z_FvXG6w5jTwpNB!D;*r zj7Nusnd4d?m^laL&8gQu)1{{223Eqo5lHx1E8G6=kr72mK8YM9qjSPx|k)cRmbfm675s(7)qLeW=^*6qZ(B~@f5Xjan z9bY(Z^PhUS{D(o{&68iHaX8tqkv`ozS55{u=a;VvXy-%?mZ&=@Z)_h*HqmoSAkPS2t!*@rG&2FH52N zrObRGn*+tgfehh_2AZnG!Z^MAltdzj6PJpGV1`&_h^D+)EU;uPnoH1ObrA7iZzY%I zK5I2G=Y+MCKxVCEMrfOtnA2pdAdr5WlR#27a+_SRksI-^HW%TnH|=at`Y+Bl+sRb# zx33`x{K!ta_A5K-8Yn6CWkg%3peeU3$w6^%PmT<2DX|~Oka3sMlncuQ#N8{4R0-)7 zEc?aCNK#Bj`f}v3%uQ2eSdp|iZ)H`rr8Lzbr#>tplb!D%gWKdFx1ZvuBx3ZcgG|zU z4l<4lSyHu58=+^b(@7jOmh?drki@<5-W=|xzV9R%UUrh!?>MUnTYi^^*jQY8WBEhr z(>s^3=Ut?+v#wI&q%o)Sggr^d@h2Y3rOQ<6ZOd2!RmLSrN!+FE$M?r@4#{#uIxJE+ z=bS(|b*MChDMsEYwJ&EH2v@Obds4mF6NyEKdLt`dR#?*LE@@cCG!U+4Xv!x`QczTq zPm4Pvy?ra6C|I_`opBFfWZW%m%h0w)LsK5C&Hsg(DnWWzON+Z&Nh*=7EA5#D_r!u5 z;GsbaZmmIUqW(R>3R;yF=sV|pn$wi#G#t-wHeWZ@7+*1hyq>%&c8XcU?qa>V8C{*$ zMN!WV53p9R7rfv=E!P;A`tw`#MPL-lmBi+Fd;s63uUr6bqmpQiMS7x7#$&yx>;iD* zNwq?GY**jc(Xk(LRy25nv7z&q%{KHML93`Q-K2S~TK6G+7+Pnq-i z8>vxLbd~9c-}dfit)UQ@*prOxj75?E4@{BTd7aS%B>bMYpe^&XLI3E6P1`rGd(=~l zt{lT=3>QEll0Go8=tYkfqAP#YZAaH`FzXHD{9PwH{}(4b_nc4?%1$MwB%dl5lF|Li zCD*pZlhLK$=7Vz*NK4NdK)?>?x)75&6yrfs_v1O+xZeLdGG2;AKt;bgDvCmJ? zs?~bz{$oe3daQlfi4q9^pX#y2*Ofr{Oe_4d-tc)k(8F!8Z29kG8_cLwWeH7XL%(Q) zYcsbw(zipRTS1|ANOY@6X@?RaINc5_mRG{ER25&Wg#CU=93F`e#rF*5sJzjgc1UzT zNZJ93J{CQ(16C6T2GRy_shxfs!rj}-of&#(2h6CKaWwwC0|p5XsyiUjgQL!lEI+@O zJ?s4rNOZ60b_XPGSXAE$8wiU}cV^8x-I+z=qfSV?@gPqZBziB@-UVw3^{FoCsb97T zWy#Y)Q=%+|{bHhLL>Ux^;`?%h?C)J!o{QbEgs`x!8`crf;ciI0b)ie$B(+JQWwNDZ zQsVA^-j)F8G5n6=s20jRpkJH!zV5QFpZSDop(nIgw6f-qhBf`nRF)s6T2Qsby{ujh zaE}1Brh&VjN1HE#4QgqSTL;wn5QfohKleC|-zN8=Vjd>ck|1{poH@lAT2;dl?qATy z{Dmq2Cn@#-Yt$WP{z(rqKc>sI=gb{uuj!PgUHcsP#BdgzG?eMb@#E*o8NT&ffc?tY z13K$=ANu&Wd?ot9Yn%-fqnp#*lj?!jxjMiG#~O9kO=akq1KLpE>zobEyve<#e)l-{ z5I~XRT!UV3q4by(MX5JWaDtZ3KSS+Cn6`k9yv40SuLd|Teb74gCG?Y7UO>&WJX^B3 zvb&YuZSg%LLy^JgP;&FeO`Da0D6pYxQ=AihbCx&ZeucNA<~g3VCnF<$(d0HP^l)#v z_h8Z3gRJ#yKFz@pIy%J_A^RL}MC)%eJbPCS0w3_DOYl7XSCamXK0Rf4jEP~6Bk0I9_aM4C$r(_~ zoGBkY(Wckk#|nzmbM6|$4bffh$X^Vd=zNEsL5F_BL-phvoDTQ06nF$ZU(c;oVLj&r Qz%T=Z&)k#JFkVOh0gp3@0{{R3 delta 6972 zcmeHLeQZz;ehxkE$ihK4pgV>7;qVK|4)eDGt3%e;_S{;f$85{=9; z_!loYJisZKV~JOkJf66Kf1p=EqBiD_pJE`Ar7?qYFoW{iqB6e>x%5i}=JN{Gm3$-^>OI!j>V|!8yA-%W{fO$ zi82+g^#oQ3RS5jHNYCS0kbk?_j^HRwB0{M*@d)LsCJv$e(1cnzk-fG!d=0=S?qo~2es)^PCgRil~Ra(%lSZOG_*U&5B5i*Su^d(EmSM6ckhN@wQ2*6h#C^~i(^s|jo=6o%>D6N*H#IB_wTtCWc?^%0mGih0u7g4qOZ zW*ZX!wPti%?lG4jJTIFI5XyuZ%?M#}BRqafAwt<<$ww%03tA>`TF^rLv&D`$>z16Y z6#j#=tMkxQ@6B6+81P~q>e@SbsB5^jph6aHu8_bSUcp<*@9xZ!p^etO{W2Mclfdjy z#>4NNU!;hWcCc6E1A|dM8tBfFL#KnloDfJz@y!oaRzP6Y-uNvmnrwp&4X)pYmS4!W z6p7JMn>Xe2WZ6j5p@z%MKdRl_MQA9)aK8-!;FxV*LC0atp=y z<$P58RDKbnOSBiu3QH_xrNR<@(&?Q|*&#cs>@|CV?4%Kk^irNg43@97v3KOStMPw6c%B{eR$$e6`+>BUXX`aN=f>JD!wz&8w_^^=D)1O zxYl8mG;D>V|3Di!=(U|4kfMBW6zFKoH9vq#MFgC>^Y1XF|5{K*Oh9&qs7&o z>LTid;wZJ9a;p-ma_l+f$+)qhH);QnFe>STsvgShcH>X3PoBF*zK?_EMokX5d{ae( zH!kZd!PR1=8gL~_4LFpb84$lt^2zdB#JeA}jPe%7yaiSLOL@z%j`V}vtK^P2*Kl4u z$fq$6aGwc^;fBko=lTdXGH#F^}%swAJ(kMX#@jcR%JaLuO z;2^YBlMAlgR6PPh*T}V_rU?=*6u_*xib zrUaoDMnD8lwJ?RUe$`XdpXygdQQ!v#`@{Xa!t?jX9x&CyATtUOS{Y>81dp~dC5VC^ zNdat#r57AHW6eK5f!A9Zc~|QJe{5wc5f4h*7-Uig?QI!;em9fr?KTFPF~OZS2DxED zRXbCSXx!JH!F9Gh!-$XD8RR7f6mMnZ6OsqCY-P$2`1n?)7`~v)c{?j^BQV}8z=yC% z=U}Lh5BKlM60*N-&G4M>V62FS%^gfRLVCJ`L0+W5#SYZ8F|KZ`u5L_VXTE0yf?dMm zcQE{pVWa+2?I+r&w7GPb<|XxQbrW@lDpie;9}rf>CQu@<-%jp-fvv^U&2F8*Uc`wf z*2b14Z}{1I9DKc&jU}C{*e~Kq%EMk)<8%V&?mEs0w2-8qVgJE+;nN~RT)_-G^q=YX z>27F`Xai7LLes1%Qolm&r#7f=5Lc8V*mssZS*cx#9I9tEgeqc|m zhBDXGD88{D84ey=w|@O#|E__DgMHhAgR^6ToQ_*S{fC4lS59?tB{T;=;P?c&1U#S6 zlHjqAR5?JmMr{N)&e8<<@oCxxo}bVaaN+)F(8WcAd!uENOiIobifoDo9R0RHkj(lU#cD`9D-u?B$dS9<%Gr*T{* zuIS9P_F$LGeZUa&Q)W?Aj6RShU77yjIZ}#djq5$56+q}Cl_{MM?j%b9_q(J8u(wH- zN{at((07wCgR!6MU10Jw{S|ZqegC?SP0`kY+VffyxP6>`jH-++SDDv4!Eqa70WX|j zEy*&W%;TP%Tvq*MnczsktO~iv7Fd zTpdd{Bxvrl+OaxetnSfg>j(=?Suv*5a9&rV{gZYL{XO*&^(N{(Wlb0xrc~NtYwQb@ zc~gaAxF!{H4%9Sh)L{HBNrSd=R*iE~w87WLSq-Ra(wG201^=ZPXIaqJq|p$==GaQri4K!RQY1tmPfbGq2lFpzL>1{vWtXUy# zd+o3@RtKH0x#zs8T-t9PeNz2)?1}^jJ3?KV+aq1yxJ`19NNq$=+M>w=N5)ybQ*tZK z0X { db.prepare( - /*sql*/ ` - create table "TournamentLFGGroup" ( - "id" integer primary key, - "tournamentId" integer not null, - "tournamentTeamId" integer, - "visibility" text, - "chatCode" text not null, - "createdAt" integer default (strftime('%s', 'now')) not null, - foreign key ("tournamentId") references "Tournament"("id") on delete cascade, - foreign key ("tournamentTeamId") references "TournamentTeam"("id") on delete cascade - ) strict - `, + /*sql*/ `alter table "TournamentTeam" add column "isLooking" integer not null default 0`, + ).run(); + // xxx: index on isLooking too to get efficient queries? problem -> we query tournamentTeamMember.userId (different table) + db.prepare( + /*sql*/ `alter table "TournamentTeam" add column "isPlaceholder" integer not null default 0`, + ).run(); + db.prepare( + /*sql*/ `alter table "TournamentTeam" add column "lfgVisibility" text`, + ).run(); + db.prepare( + /*sql*/ `alter table "TournamentTeam" add column "lfgNote" text`, + ).run(); + db.prepare( + /*sql*/ `alter table "TournamentTeam" add column "chatCode" text`, ).run(); db.prepare( - /*sql*/ `create index tournament_lfg_group_tournament_id on "TournamentLFGGroup"("tournamentId")`, + /*sql*/ `alter table "TournamentTeamMember" add column "role" text not null default 'REGULAR'`, ).run(); db.prepare( - /*sql*/ `create index tournament_lfg_group_tournament_team_id on "TournamentLFGGroup"("tournamentTeamId")`, + /*sql*/ `alter table "TournamentTeamMember" add column "isStayAsSub" integer not null default 0`, ).run(); db.prepare( - /*sql*/ ` - create table "TournamentLFGGroupMember" ( - "groupId" integer not null, - "tournamentId" integer not null, - "userId" integer not null, - "role" text not null, - "note" text, - "isStayAsSub" integer default 0 not null, - "createdAt" integer default (strftime('%s', 'now')) not null, - foreign key ("groupId") references "TournamentLFGGroup"("id") on delete cascade, - foreign key ("userId") references "User"("id") on delete cascade, - unique("tournamentId", "userId") on conflict rollback - ) strict - `, - ).run(); - - db.prepare( - /*sql*/ `create index tournament_lfg_group_member_group_id on "TournamentLFGGroupMember"("groupId")`, - ).run(); - db.prepare( - /*sql*/ `create index tournament_lfg_group_member_user_id on "TournamentLFGGroupMember"("userId")`, + /*sql*/ `update "TournamentTeamMember" set "role" = 'OWNER' where "isOwner" = 1`, ).run(); db.prepare( /*sql*/ ` create table "TournamentLFGLike" ( - "likerGroupId" integer not null, - "targetGroupId" integer not null, + "likerTeamId" integer not null, + "targetTeamId" integer not null, "createdAt" integer default (strftime('%s', 'now')) not null, - foreign key ("likerGroupId") references "TournamentLFGGroup"("id") on delete cascade, - foreign key ("targetGroupId") references "TournamentLFGGroup"("id") on delete cascade, - unique("likerGroupId", "targetGroupId") on conflict rollback + foreign key ("likerTeamId") references "TournamentTeam"("id") on delete cascade, + foreign key ("targetTeamId") references "TournamentTeam"("id") on delete cascade, + unique("likerTeamId", "targetTeamId") on conflict rollback ) strict `, ).run(); db.prepare( - /*sql*/ `create index tournament_lfg_like_liker_group_id on "TournamentLFGLike"("likerGroupId")`, + /*sql*/ `create index tournament_lfg_like_liker_team_id on "TournamentLFGLike"("likerTeamId")`, ).run(); db.prepare( - /*sql*/ `create index tournament_lfg_like_target_group_id on "TournamentLFGLike"("targetGroupId")`, + /*sql*/ `create index tournament_lfg_like_target_team_id on "TournamentLFGLike"("targetTeamId")`, ).run(); })(); }