mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-01 23:58:36 -05:00
Move app to apps/web-react in pnpm workspace layout
This commit is contained in:
107
apps/web-react/app/db/seed/dev/badges.ts
Normal file
107
apps/web-react/app/db/seed/dev/badges.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { BADGE } from "~/features/badges/badges-constants";
|
||||
import { faker } from "../core/faker";
|
||||
import badges from "../data/badges.json";
|
||||
import * as BadgeFactory from "../factories/BadgeFactory";
|
||||
import * as UserFactory from "../factories/UserFactory";
|
||||
import type { SeededUsers } from "./users";
|
||||
|
||||
const HOMEMADE_BADGE_COUNT = 5;
|
||||
const NZAP_BADGE_COUNT = 20;
|
||||
const ADMIN_BADGE_COUNT = 3;
|
||||
const NZAP_FAVORITE_BADGE_COUNT = BADGE.SMALL_BADGES_PER_DISPLAY_PAGE + 1;
|
||||
const MANY_OWNERS_COUNT = 60;
|
||||
const MANY_BADGES_USER_BADGE_COUNT = 25;
|
||||
|
||||
export type SeededBadges = {
|
||||
ids: number[];
|
||||
};
|
||||
|
||||
export async function seedBadges(users: SeededUsers): Promise<SeededBadges> {
|
||||
const ownableUserIds = [
|
||||
...users.showcaseIds,
|
||||
...users.showcaseIds,
|
||||
...users.crowdIds,
|
||||
];
|
||||
const manyBadgesUserId = users.favoriteBadgeUserIds[0];
|
||||
|
||||
const ids: number[] = [];
|
||||
for (const [i, badge] of badges.entries()) {
|
||||
const created = await BadgeFactory.create(
|
||||
{
|
||||
code: badge.code,
|
||||
displayName: badge.displayName,
|
||||
hue: badge.hue,
|
||||
authorId: i < HOMEMADE_BADGE_COUNT ? users.adminId : null,
|
||||
},
|
||||
{
|
||||
ownerIds: fakeOwnerIds({
|
||||
index: i,
|
||||
users,
|
||||
ownableUserIds,
|
||||
manyBadgesUserId,
|
||||
}),
|
||||
managerIds:
|
||||
i < HOMEMADE_BADGE_COUNT
|
||||
? [users.adminId]
|
||||
: i < 10
|
||||
? [users.nzapId]
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
ids.push(created.id);
|
||||
}
|
||||
|
||||
await seedFavoriteBadges(users, ids);
|
||||
|
||||
return { ids };
|
||||
}
|
||||
|
||||
function fakeOwnerIds({
|
||||
index,
|
||||
users,
|
||||
ownableUserIds,
|
||||
manyBadgesUserId,
|
||||
}: {
|
||||
index: number;
|
||||
users: SeededUsers;
|
||||
ownableUserIds: number[];
|
||||
manyBadgesUserId: number;
|
||||
}) {
|
||||
const ownerIds = faker.helpers.arrayElements(
|
||||
ownableUserIds,
|
||||
index === 0 ? MANY_OWNERS_COUNT : faker.number.int({ min: 1, max: 24 }),
|
||||
);
|
||||
|
||||
if (index < NZAP_BADGE_COUNT) {
|
||||
ownerIds.push(users.nzapId);
|
||||
}
|
||||
|
||||
if (index < ADMIN_BADGE_COUNT) {
|
||||
ownerIds.push(users.adminId);
|
||||
}
|
||||
|
||||
if (index < MANY_BADGES_USER_BADGE_COUNT) {
|
||||
ownerIds.push(manyBadgesUserId);
|
||||
}
|
||||
|
||||
// favorite badges are picked from the first few, so their pickers own them
|
||||
if (index < 3) {
|
||||
ownerIds.push(...users.favoriteBadgeUserIds);
|
||||
}
|
||||
|
||||
return [...new Set(ownerIds)];
|
||||
}
|
||||
|
||||
async function seedFavoriteBadges(users: SeededUsers, badgeIds: number[]) {
|
||||
for (const [i, userId] of users.favoriteBadgeUserIds.entries()) {
|
||||
await UserFactory.updateProfile(userId, {
|
||||
favoriteBadgeIds: [badgeIds[i % 3]],
|
||||
});
|
||||
}
|
||||
|
||||
// a supporter picks a whole row of small badges alongside the big one
|
||||
await UserFactory.updateProfile(users.nzapId, {
|
||||
favoriteBadgeIds: badgeIds.slice(0, NZAP_FAVORITE_BADGE_COUNT),
|
||||
});
|
||||
}
|
||||
39
apps/web-react/app/db/seed/dev/builds.ts
Normal file
39
apps/web-react/app/db/seed/dev/builds.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { faker } from "../core/faker";
|
||||
import * as showcaseNames from "../core/showcaseNames";
|
||||
import * as BuildFactory from "../factories/BuildFactory";
|
||||
import type { SeededUsers } from "./users";
|
||||
|
||||
const NZAP_BUILD_COUNT = 100;
|
||||
const SPLATTERSHOT_ID = 40;
|
||||
const ONE_WEAPON_BUILD_COUNT = 40;
|
||||
const CROWD_BUILD_COUNT = 360;
|
||||
|
||||
export async function seedBuilds(users: SeededUsers) {
|
||||
await BuildFactory.createMany(NZAP_BUILD_COUNT, (i) => ({
|
||||
ownerId: users.nzapId,
|
||||
title: showcaseNames.buildTitle(),
|
||||
isPrivate: i % 8 === 0 ? (1 as const) : (0 as const),
|
||||
}));
|
||||
|
||||
// one weapon with builds from many users, so its weapon page paginates
|
||||
const splattershotOwners = faker.helpers.arrayElements(
|
||||
[...users.showcaseIds, ...users.crowdIds],
|
||||
ONE_WEAPON_BUILD_COUNT,
|
||||
);
|
||||
for (const ownerId of splattershotOwners) {
|
||||
await BuildFactory.create({
|
||||
ownerId,
|
||||
weaponSplIds: [SPLATTERSHOT_ID],
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < CROWD_BUILD_COUNT; i++) {
|
||||
await BuildFactory.create({
|
||||
ownerId: faker.helpers.arrayElement([
|
||||
...users.showcaseIds,
|
||||
...users.crowdIds,
|
||||
]),
|
||||
isPrivate: faker.number.float(1) < 0.05 ? (1 as const) : (0 as const),
|
||||
});
|
||||
}
|
||||
}
|
||||
134
apps/web-react/app/db/seed/dev/calendar.ts
Normal file
134
apps/web-react/app/db/seed/dev/calendar.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { faker } from "../core/faker";
|
||||
import * as showcaseNames from "../core/showcaseNames";
|
||||
import * as CalendarEventFactory from "../factories/CalendarEventFactory";
|
||||
import * as CalendarEventResultFactory from "../factories/CalendarEventResultFactory";
|
||||
import type { SeededBadges } from "./badges";
|
||||
import type { SeededUsers } from "./users";
|
||||
|
||||
const EVENT_COUNT = 200;
|
||||
const RESULT_TARGET_USER_RESULTS = 8;
|
||||
/** Results N-ZAP is placed in, enough for his results page to paginate. */
|
||||
const NZAP_RESULT_COUNT = 30;
|
||||
|
||||
export type SeededCalendarEvents = {
|
||||
/** Result teams N-ZAP played on, in the order they were reported. */
|
||||
nzapResultTeamIds: number[];
|
||||
};
|
||||
|
||||
export async function seedCalendarEvents(
|
||||
users: SeededUsers,
|
||||
badges: SeededBadges,
|
||||
): Promise<SeededCalendarEvents> {
|
||||
const authorPool = [...users.showcaseIds, ...users.crowdIds];
|
||||
const nzapResultTeamIds: number[] = [];
|
||||
|
||||
for (let i = 0; i < EVENT_COUNT; i++) {
|
||||
const startTime = fakeStartTime(i);
|
||||
|
||||
const event = await CalendarEventFactory.create({
|
||||
name: showcaseNames.eventName(),
|
||||
authorId: i === 0 ? users.nzapId : faker.helpers.arrayElement(authorPool),
|
||||
badges:
|
||||
faker.number.float(1) < 0.25
|
||||
? faker.helpers.arrayElements(badges.ids, { min: 1, max: 3 })
|
||||
: [],
|
||||
startTimes: fakeStartTimes(startTime),
|
||||
// the rest fall back to the default logo, as an event without one does
|
||||
hasAvatar: faker.number.float(1) < 0.4,
|
||||
});
|
||||
|
||||
const isPast = startTime.getTime() < Date.now();
|
||||
if (isPast && faker.number.float(1) < 0.6) {
|
||||
const withNzap = nzapResultTeamIds.length < NZAP_RESULT_COUNT;
|
||||
|
||||
const result = await CalendarEventResultFactory.create({
|
||||
eventId: event.id,
|
||||
results: fakeResults(users, withNzap ? users.nzapId : null),
|
||||
});
|
||||
|
||||
if (withNzap) {
|
||||
nzapResultTeamIds.push(nzapTeamId(result.teams, users.nzapId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { nzapResultTeamIds };
|
||||
}
|
||||
|
||||
function nzapTeamId(
|
||||
teams: Awaited<ReturnType<typeof CalendarEventResultFactory.create>>["teams"],
|
||||
nzapId: number,
|
||||
) {
|
||||
const team = teams.find((team) =>
|
||||
team.players.some((player) => player.id === nzapId),
|
||||
);
|
||||
invariant(team, "N-ZAP was not placed in the results");
|
||||
|
||||
return team.id;
|
||||
}
|
||||
|
||||
function fakeStartTime(index: number) {
|
||||
const startTime =
|
||||
index % 2 === 0
|
||||
? faker.date.soon({ days: 42 })
|
||||
: faker.date.recent({ days: 240 });
|
||||
startTime.setMinutes(0, 0, 0);
|
||||
|
||||
return startTime;
|
||||
}
|
||||
|
||||
function fakeStartTimes(startTime: Date) {
|
||||
const isTwoDayEvent = faker.number.float(1) < 0.1;
|
||||
if (!isTwoDayEvent) return [dateToDatabaseTimestamp(startTime)];
|
||||
|
||||
const secondDay = new Date(startTime);
|
||||
secondDay.setDate(secondDay.getDate() + 1);
|
||||
|
||||
return [
|
||||
dateToDatabaseTimestamp(startTime),
|
||||
dateToDatabaseTimestamp(secondDay),
|
||||
];
|
||||
}
|
||||
|
||||
function fakeResults(users: SeededUsers, nzapId: number | null) {
|
||||
const placementCount = faker.helpers.arrayElement([1, 2, 3, 3, 3, 8]);
|
||||
const usedUserIds = new Set<number>();
|
||||
// spread over the placements, so his results are not all of the same podium spot
|
||||
const nzapPlacementIdx = faker.number.int({ max: placementCount - 1 });
|
||||
|
||||
const drawUserId = () => {
|
||||
// weighted toward the showcase set so their result lists paginate
|
||||
const pool =
|
||||
faker.number.float(1) < 0.7
|
||||
? users.showcaseIds.slice(0, RESULT_TARGET_USER_RESULTS * 4)
|
||||
: users.crowdIds;
|
||||
const userId = faker.helpers.arrayElement(
|
||||
pool.filter((id) => !usedUserIds.has(id)),
|
||||
);
|
||||
usedUserIds.add(userId);
|
||||
|
||||
return userId;
|
||||
};
|
||||
|
||||
return Array.from({ length: placementCount }, (_, i) => ({
|
||||
placement: i + 1,
|
||||
teamName: showcaseNames.teamName(),
|
||||
players: [
|
||||
...(nzapId !== null && i === nzapPlacementIdx
|
||||
? [{ name: null, userId: nzapId }]
|
||||
: []),
|
||||
...Array.from(
|
||||
{ length: faker.helpers.arrayElement([1, 2, 3, 4, 4, 4, 5]) },
|
||||
() => {
|
||||
const isUnregisteredPlayer = faker.number.float(1) < 0.2;
|
||||
|
||||
return isUnregisteredPlayer
|
||||
? { name: faker.person.firstName(), userId: null }
|
||||
: { name: null, userId: drawUserId() };
|
||||
},
|
||||
),
|
||||
],
|
||||
}));
|
||||
}
|
||||
31
apps/web-react/app/db/seed/dev/highlights.ts
Normal file
31
apps/web-react/app/db/seed/dev/highlights.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import * as ResultHighlightFactory from "../factories/ResultHighlightFactory";
|
||||
import type { SeededCalendarEvents } from "./calendar";
|
||||
import type { SeededTournaments } from "./tournaments";
|
||||
import type { SeededUsers } from "./users";
|
||||
|
||||
/** Calendar event results N-ZAP highlights, few enough that the highlights view of
|
||||
* his results page is a page shorter than the full one. */
|
||||
const NZAP_CALENDAR_HIGHLIGHT_COUNT = 6;
|
||||
|
||||
/**
|
||||
* Highlights some of N-ZAP's results, so that his profile has a highlighted results
|
||||
* widget and his results page opens on the highlights view.
|
||||
*/
|
||||
export async function seedResultHighlights({
|
||||
users,
|
||||
calendarEvents,
|
||||
tournaments,
|
||||
}: {
|
||||
users: SeededUsers;
|
||||
calendarEvents: SeededCalendarEvents;
|
||||
tournaments: SeededTournaments;
|
||||
}) {
|
||||
await ResultHighlightFactory.replaceAll({
|
||||
userId: users.nzapId,
|
||||
resultTeamIds: calendarEvents.nzapResultTeamIds.slice(
|
||||
0,
|
||||
NZAP_CALENDAR_HIGHLIGHT_COUNT,
|
||||
),
|
||||
resultTournamentTeamIds: tournaments.nzapTeamIds,
|
||||
});
|
||||
}
|
||||
219
apps/web-react/app/db/seed/dev/misc.ts
Normal file
219
apps/web-react/app/db/seed/dev/misc.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { sub } from "date-fns";
|
||||
import { Config } from "~/config";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import type { Notification } from "~/features/notifications/notifications-types";
|
||||
import type { MainWeaponId, ModeShort } from "~/modules/in-game-lists/types";
|
||||
import {
|
||||
getArtFilename,
|
||||
SEED_ART_URLS,
|
||||
} from "../../../../scripts/seed-art-urls";
|
||||
import { NZAP_TEST_DISCORD_ID } from "../constants";
|
||||
import { faker } from "../core/faker";
|
||||
import placements from "../data/placements.json";
|
||||
import * as ArtFactory from "../factories/ArtFactory";
|
||||
import * as FriendRequestFactory from "../factories/FriendRequestFactory";
|
||||
import * as FriendshipFactory from "../factories/FriendshipFactory";
|
||||
import * as LiveStreamFactory from "../factories/LiveStreamFactory";
|
||||
import * as NotificationFactory from "../factories/NotificationFactory";
|
||||
import * as SplatoonRotationFactory from "../factories/SplatoonRotationFactory";
|
||||
import * as UserReportFactory from "../factories/UserReportFactory";
|
||||
import * as XRankPlacementFactory from "../factories/XRankPlacementFactory";
|
||||
import type { SeededSendouQ } from "./sendouq";
|
||||
import type { SeededTournaments } from "./tournaments";
|
||||
import type { SeededUsers } from "./users";
|
||||
|
||||
const NZAP_PLAYER_SPL_ID = "qx6imlx72tfeqrhqfnmm";
|
||||
const FRIEND_COUNT = 8;
|
||||
const STREAM_COUNT = 20;
|
||||
|
||||
export async function seedMisc({
|
||||
users,
|
||||
sendouq,
|
||||
tournaments,
|
||||
}: {
|
||||
users: SeededUsers;
|
||||
sendouq: SeededSendouQ;
|
||||
tournaments: SeededTournaments;
|
||||
}) {
|
||||
await seedXRankPlacements(users);
|
||||
await seedArts(users);
|
||||
await seedFriends(users);
|
||||
await seedNotifications(users, tournaments);
|
||||
await seedUserReports(users, sendouq);
|
||||
|
||||
await LiveStreamFactory.replaceAll(
|
||||
users.showcaseIds.slice(0, STREAM_COUNT).map((userId) => ({ userId })),
|
||||
);
|
||||
await SplatoonRotationFactory.replaceAll();
|
||||
}
|
||||
|
||||
async function seedXRankPlacements(users: SeededUsers) {
|
||||
// a top 500 player who is a site user without plus membership
|
||||
const unaffiliatedTopPlayerId = users.crowdIds[users.crowdIds.length - 1];
|
||||
|
||||
for (const [i, placement] of placements.entries()) {
|
||||
const playerUserId =
|
||||
placement.playerSplId === NZAP_PLAYER_SPL_ID
|
||||
? users.nzapId
|
||||
: i === 0
|
||||
? unaffiliatedTopPlayerId
|
||||
: undefined;
|
||||
|
||||
await XRankPlacementFactory.create(
|
||||
{
|
||||
...placement,
|
||||
mode: placement.mode as ModeShort,
|
||||
region: placement.region as Tables["XRankPlacement"]["region"],
|
||||
weaponSplId: placement.weaponSplId as MainWeaponId,
|
||||
playerUserId,
|
||||
},
|
||||
{ refreshPeakXp: i === placements.length - 1 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function seedArts(users: SeededUsers) {
|
||||
let nextUrl = 0;
|
||||
|
||||
for (const authorId of [users.nzapId, ...users.artistIds]) {
|
||||
const artCount = faker.helpers.arrayElement([1, 2, 3, 3, 4]);
|
||||
|
||||
for (let i = 0; i < artCount; i++) {
|
||||
await ArtFactory.create({
|
||||
authorId,
|
||||
url: getArtFilename(nextUrl++ % SEED_ART_URLS.length),
|
||||
description:
|
||||
faker.number.float(1) < 0.5 ? faker.lorem.paragraph() : null,
|
||||
linkedUsers:
|
||||
i === 1
|
||||
? [
|
||||
...(authorId === users.nzapId ? [] : [users.nzapId]),
|
||||
...faker.helpers.arrayElements(users.showcaseIds, {
|
||||
min: 0,
|
||||
max: 2,
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function seedFriends(users: SeededUsers) {
|
||||
const friendIds = users.showcaseIds.slice(0, FRIEND_COUNT);
|
||||
|
||||
for (const friendId of friendIds) {
|
||||
await FriendshipFactory.create({
|
||||
userOneId: users.nzapId,
|
||||
userTwoId: friendId,
|
||||
});
|
||||
}
|
||||
|
||||
// mutual friends between some of them, so user cards show the overlap
|
||||
await FriendshipFactory.create({
|
||||
userOneId: friendIds[0],
|
||||
userTwoId: friendIds[1],
|
||||
});
|
||||
await FriendshipFactory.create({
|
||||
userOneId: friendIds[0],
|
||||
userTwoId: friendIds[2],
|
||||
});
|
||||
|
||||
await FriendRequestFactory.create({
|
||||
senderId: users.showcaseIds[FRIEND_COUNT],
|
||||
receiverId: users.nzapId,
|
||||
});
|
||||
}
|
||||
|
||||
async function seedNotifications(
|
||||
users: SeededUsers,
|
||||
tournaments: SeededTournaments,
|
||||
) {
|
||||
const { id: tournamentId, name: tournamentName } = tournaments.regOpen;
|
||||
|
||||
const notifications: Notification[] = [
|
||||
{ type: "PLUS_SUGGESTION_ADDED", meta: { tier: 1 } },
|
||||
{ type: "SEASON_STARTED", meta: { seasonNth: 1 } },
|
||||
{
|
||||
type: "TO_ADDED_TO_TEAM",
|
||||
meta: {
|
||||
adderUsername: "N-ZAP",
|
||||
teamName: "Chimera",
|
||||
tournamentId,
|
||||
tournamentName,
|
||||
tournamentTeamId: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "TO_BRACKET_STARTED",
|
||||
meta: {
|
||||
tournamentId,
|
||||
tournamentName,
|
||||
bracketIdx: 0,
|
||||
bracketName: "Main Bracket",
|
||||
},
|
||||
},
|
||||
{ type: "BADGE_ADDED", meta: { badgeName: "4v4 Sundaes", badgeId: 1 } },
|
||||
{
|
||||
type: "TAGGED_TO_ART",
|
||||
meta: {
|
||||
adderUsername: "N-ZAP",
|
||||
adderDiscordId: NZAP_TEST_DISCORD_ID,
|
||||
artId: 1,
|
||||
},
|
||||
},
|
||||
{ type: "SQ_ADDED_TO_GROUP", meta: { adderUsername: "N-ZAP" } },
|
||||
{ type: "SQ_NEW_MATCH", meta: { matchId: 100 } },
|
||||
{ type: "PLUS_VOTING_STARTED", meta: { seasonNth: 1 } },
|
||||
{
|
||||
type: "TO_CHECK_IN_OPENED",
|
||||
meta: { tournamentId, tournamentName },
|
||||
pictureUrl: `${Config.staticAssetsUrl}/img/tournament-logos/pn.avif`,
|
||||
},
|
||||
];
|
||||
|
||||
for (const [i, notification] of notifications.entries()) {
|
||||
const createdAt = sub(new Date(), {
|
||||
days: notifications.length - 1 - i,
|
||||
minutes: i * 17,
|
||||
});
|
||||
|
||||
await NotificationFactory.create(
|
||||
{
|
||||
notification,
|
||||
users: [
|
||||
{ userId: users.adminId, seen: i <= 7 ? 1 : 0 },
|
||||
{ userId: users.nzapId, seen: i <= 7 ? 1 : 0 },
|
||||
],
|
||||
},
|
||||
{ createdAt },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function seedUserReports(users: SeededUsers, sendouq: SeededSendouQ) {
|
||||
// uneven spread over the trailing year so the admin tab's bar graph shows variety
|
||||
const monthsAgoDistribution = [
|
||||
0, 0, 0, 1, 2, 2, 2, 2, 5, 5, 7, 8, 10, 11, 11,
|
||||
];
|
||||
|
||||
for (const [i, monthsAgo] of monthsAgoDistribution.entries()) {
|
||||
await UserReportFactory.create(
|
||||
{
|
||||
reportedUserId: users.nzapId,
|
||||
reporterUserId: users.showcaseIds[10 + i],
|
||||
matchId:
|
||||
i % 3 === 0
|
||||
? faker.helpers.arrayElement(sendouq.recentMatchIds)
|
||||
: null,
|
||||
},
|
||||
{
|
||||
createdAt: sub(new Date(), {
|
||||
months: monthsAgo,
|
||||
days: (i * 3) % 7,
|
||||
hours: i,
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
38
apps/web-react/app/db/seed/dev/organizations.ts
Normal file
38
apps/web-react/app/db/seed/dev/organizations.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as TournamentOrganizationFactory from "../factories/TournamentOrganizationFactory";
|
||||
import type { SeededUsers } from "./users";
|
||||
|
||||
export type SeededOrganization = {
|
||||
id: number;
|
||||
name: string;
|
||||
seriesNames: string[];
|
||||
};
|
||||
|
||||
export async function seedOrganizations(
|
||||
users: SeededUsers,
|
||||
): Promise<SeededOrganization[]> {
|
||||
const created = await TournamentOrganizationFactory.create(
|
||||
{ name: "sendou.ink", ownerId: users.adminId },
|
||||
{
|
||||
avatarFileName: "default.png",
|
||||
description: "Sendou.ink official tournaments",
|
||||
socials: [
|
||||
"https://bsky.app/profile/sendou.ink",
|
||||
"https://twitch.tv/sendou",
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: "PICNIC",
|
||||
description: "PICNIC tournament series",
|
||||
showLeaderboard: false,
|
||||
},
|
||||
],
|
||||
members: [
|
||||
{ userId: users.orgAdminId, role: "ADMIN" },
|
||||
{ userId: users.nzapId, role: "MEMBER" },
|
||||
],
|
||||
isEstablished: true,
|
||||
},
|
||||
);
|
||||
|
||||
return [{ id: created.id, name: "sendou.ink", seriesNames: ["PICNIC"] }];
|
||||
}
|
||||
75
apps/web-react/app/db/seed/dev/plus.ts
Normal file
75
apps/web-react/app/db/seed/dev/plus.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { lastCompletedVoting } from "~/features/plus-voting/core/voting-time";
|
||||
import {
|
||||
PLUS_DOWNVOTE,
|
||||
PLUS_UPVOTE,
|
||||
} from "~/features/plus-voting/plus-voting-constants";
|
||||
import { faker } from "../core/faker";
|
||||
import * as PlusSuggestionFactory from "../factories/PlusSuggestionFactory";
|
||||
import * as PlusVoteFactory from "../factories/PlusVoteFactory";
|
||||
import type { SeededUsers } from "./users";
|
||||
|
||||
const TIER_SIZES = [30, 50, 70];
|
||||
const FAILED_PER_TIER = 8;
|
||||
const SUGGESTED_COUNT = 110;
|
||||
|
||||
export async function seedPlus(users: SeededUsers) {
|
||||
const memberIds = await seedLastMonthsVoting(users);
|
||||
await PlusVoteFactory.syncTiers();
|
||||
|
||||
await seedSuggestions(users, memberIds);
|
||||
}
|
||||
|
||||
async function seedLastMonthsVoting(users: SeededUsers) {
|
||||
const candidateIds = [
|
||||
users.adminId,
|
||||
...users.showcaseIds,
|
||||
...users.crowdIds.slice(0, 80),
|
||||
].filter((id) => id !== users.nzapId);
|
||||
|
||||
const memberIds: number[] = [];
|
||||
let nextCandidate = 0;
|
||||
|
||||
for (const [tierIndex, size] of TIER_SIZES.entries()) {
|
||||
const tier = tierIndex + 1;
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
const votedId = candidateIds[nextCandidate++];
|
||||
await PlusVoteFactory.create({
|
||||
authorId: users.adminId,
|
||||
votedId,
|
||||
tier,
|
||||
score: PLUS_UPVOTE,
|
||||
});
|
||||
memberIds.push(votedId);
|
||||
}
|
||||
|
||||
for (let i = 0; i < FAILED_PER_TIER; i++) {
|
||||
await PlusVoteFactory.create({
|
||||
authorId: users.adminId,
|
||||
votedId: candidateIds[nextCandidate++],
|
||||
tier,
|
||||
score: PLUS_DOWNVOTE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return memberIds;
|
||||
}
|
||||
|
||||
async function seedSuggestions(users: SeededUsers, memberIds: number[]) {
|
||||
const { month, year } = lastCompletedVoting(new Date());
|
||||
const suggestableIds = users.crowdIds.slice(100, 100 + SUGGESTED_COUNT * 2);
|
||||
|
||||
for (let i = 0; i < SUGGESTED_COUNT; i++) {
|
||||
const suggestedId = suggestableIds[i];
|
||||
const isLastMonths = i % 3 === 0;
|
||||
const suggesterId = faker.helpers.arrayElement(memberIds);
|
||||
|
||||
await PlusSuggestionFactory.create({
|
||||
authorId: suggesterId,
|
||||
suggestedId,
|
||||
tier: faker.helpers.arrayElement([1, 2, 3]),
|
||||
...(isLastMonths ? { month, year } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
138
apps/web-react/app/db/seed/dev/scrims-lfg.ts
Normal file
138
apps/web-react/app/db/seed/dev/scrims-lfg.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { add } from "date-fns";
|
||||
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import { faker } from "../core/faker";
|
||||
import * as showcaseNames from "../core/showcaseNames";
|
||||
import * as AssociationFactory from "../factories/AssociationFactory";
|
||||
import * as LFGPostFactory from "../factories/LFGPostFactory";
|
||||
import * as ScrimPostFactory from "../factories/ScrimPostFactory";
|
||||
import type { SeededTeams } from "./teams";
|
||||
import type { SeededUsers } from "./users";
|
||||
|
||||
const SCRIM_POST_COUNT = 20;
|
||||
const LFG_POST_COUNT = 9;
|
||||
const ASSOCIATION_COUNT = 3;
|
||||
|
||||
export async function seedScrimsAndLFG(users: SeededUsers, teams: SeededTeams) {
|
||||
await seedScrimPosts(users, teams);
|
||||
await seedLFGPosts(users, teams);
|
||||
await seedAssociations(users);
|
||||
}
|
||||
|
||||
async function seedScrimPosts(users: SeededUsers, teams: SeededTeams) {
|
||||
const userPool = [...users.showcaseIds, ...users.crowdIds];
|
||||
let next = 0;
|
||||
const takeUsers = (count: number) => {
|
||||
const taken = userPool.slice(next, next + count);
|
||||
next += count;
|
||||
|
||||
return taken.map((userId, i) => ({
|
||||
userId,
|
||||
isOwner: i === 0 ? (1 as const) : (0 as const),
|
||||
}));
|
||||
};
|
||||
|
||||
// an accepted scrim between the admin's and N-ZAP's rosters
|
||||
await ScrimPostFactory.create(
|
||||
{
|
||||
startsAt: dateToDatabaseTimestamp(add(new Date(), { hours: 2 })),
|
||||
isScheduledForFuture: true,
|
||||
managedByAnyone: true,
|
||||
users: [{ userId: users.adminId, isOwner: 1 }, ...takeUsers(3)],
|
||||
},
|
||||
{
|
||||
requests: [
|
||||
{
|
||||
users: [{ userId: users.nzapId, isOwner: 1 }, ...takeUsers(3)],
|
||||
isAccepted: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
for (let i = 0; i < SCRIM_POST_COUNT; i++) {
|
||||
const divs = faker.number.float(1) < 0.8 ? fakeDivRange() : null;
|
||||
const startsAt =
|
||||
faker.number.float(1) < 0.5
|
||||
? databaseTimestampNow()
|
||||
: dateToDatabaseTimestamp(
|
||||
faker.date.between({
|
||||
from: new Date(),
|
||||
to: add(new Date(), { days: 7 }),
|
||||
}),
|
||||
);
|
||||
|
||||
await ScrimPostFactory.create(
|
||||
{
|
||||
startsAt,
|
||||
isScheduledForFuture: true,
|
||||
managedByAnyone: true,
|
||||
maxDiv: divs?.maxDiv,
|
||||
minDiv: divs?.minDiv,
|
||||
teamId:
|
||||
faker.number.float(1) < 0.4
|
||||
? faker.helpers.arrayElement(teams.ids)
|
||||
: null,
|
||||
text: faker.number.float(1) < 0.5 ? showcaseNames.postText() : null,
|
||||
maps: faker.helpers.arrayElement(["SZ", "ALL", "RANKED", null, null]),
|
||||
users: takeUsers(faker.helpers.arrayElement([4, 4, 4, 5, 5, 6])),
|
||||
},
|
||||
{
|
||||
requests: i < 3 ? [{ users: takeUsers(4) }] : undefined,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function seedLFGPosts(users: SeededUsers, teams: SeededTeams) {
|
||||
const authorIds = [
|
||||
users.adminId,
|
||||
...faker.helpers.arrayElements(
|
||||
[...users.showcaseIds, ...users.crowdIds],
|
||||
LFG_POST_COUNT - 2,
|
||||
),
|
||||
];
|
||||
|
||||
for (const authorId of authorIds) {
|
||||
await LFGPostFactory.create({
|
||||
authorId,
|
||||
text: showcaseNames.postText(),
|
||||
});
|
||||
}
|
||||
|
||||
// posted by the owner of the team it is looking for players for
|
||||
await LFGPostFactory.create({
|
||||
authorId: users.nzapId,
|
||||
type: "TEAM_FOR_PLAYER",
|
||||
teamId: teams.allianceRogueId,
|
||||
timezone: "Europe/Stockholm",
|
||||
});
|
||||
}
|
||||
|
||||
async function seedAssociations(users: SeededUsers) {
|
||||
for (let i = 0; i < ASSOCIATION_COUNT; i++) {
|
||||
const ownerId =
|
||||
i === ASSOCIATION_COUNT - 1
|
||||
? faker.helpers.arrayElement(users.showcaseIds)
|
||||
: users.adminId;
|
||||
|
||||
await AssociationFactory.create(
|
||||
{ name: faker.company.name(), userId: ownerId },
|
||||
{
|
||||
memberUserIds: [
|
||||
...(ownerId === users.adminId ? [] : [users.adminId]),
|
||||
...faker.helpers.arrayElements(
|
||||
users.showcaseIds.filter((id) => id !== ownerId),
|
||||
faker.helpers.arrayElement([6, 10, 16]),
|
||||
),
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function fakeDivRange() {
|
||||
return {
|
||||
maxDiv: faker.helpers.arrayElement([0, 1, 2, 3, 4, 5]),
|
||||
minDiv: faker.helpers.arrayElement([6, 7, 8, 9, 10, 11]),
|
||||
};
|
||||
}
|
||||
262
apps/web-react/app/db/seed/dev/sendouq.ts
Normal file
262
apps/web-react/app/db/seed/dev/sendouq.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import { sub } from "date-fns";
|
||||
import { FULL_GROUP_SIZE, SENDOUQ } from "~/features/sendouq/q-constants";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { faker } from "../core/faker";
|
||||
import * as SQGroupFactory from "../factories/SQGroupFactory";
|
||||
import * as SQMatchFactory from "../factories/SQMatchFactory";
|
||||
import * as SQReportedWeaponFactory from "../factories/SQReportedWeaponFactory";
|
||||
import type { SeededTeams } from "./teams";
|
||||
import type { SeededUsers } from "./users";
|
||||
|
||||
const RECENT_MATCH_COUNT = 300;
|
||||
const OLDER_MATCH_COUNT = 140;
|
||||
const SQUAD_COUNT = 8;
|
||||
const LOOKING_GROUP_COUNT = 10;
|
||||
const REPORTED_MAP_COUNT = 4;
|
||||
|
||||
/** N-ZAP's unconfirmed match, on an id worth remembering. Every other match is
|
||||
* created before it, so the squad matches make up the difference. */
|
||||
const NZAP_MATCH_ID = 500;
|
||||
const SQUAD_MATCH_COUNT =
|
||||
NZAP_MATCH_ID - 1 - RECENT_MATCH_COUNT - OLDER_MATCH_COUNT;
|
||||
|
||||
export type SeededSendouQ = {
|
||||
recentMatchIds: number[];
|
||||
};
|
||||
|
||||
export async function seedSendouQ(
|
||||
users: SeededUsers,
|
||||
teams: SeededTeams,
|
||||
): Promise<SeededSendouQ> {
|
||||
const playerIds = users.showcaseIds;
|
||||
|
||||
const recentMatchIds: number[] = [];
|
||||
for (let i = 0; i < RECENT_MATCH_COUNT; i++) {
|
||||
const match = await seedConcludedMatch(
|
||||
playerIds,
|
||||
sub(new Date(), {
|
||||
days: faker.number.int({ min: 0, max: 60 }),
|
||||
hours: faker.number.int({ min: 0, max: 23 }),
|
||||
}),
|
||||
);
|
||||
recentMatchIds.push(match.id);
|
||||
}
|
||||
|
||||
for (let i = 0; i < OLDER_MATCH_COUNT; i++) {
|
||||
await seedConcludedMatch(
|
||||
playerIds,
|
||||
sub(new Date(), {
|
||||
days: faker.number.int({ min: 61, max: 600 }),
|
||||
hours: faker.number.int({ min: 0, max: 23 }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await seedSquadMatches(teams);
|
||||
await seedNzapReportedMatch(users, teams);
|
||||
await seedNzapCanceledMatches(users, teams);
|
||||
await seedLookingGroups(users);
|
||||
|
||||
return { recentMatchIds };
|
||||
}
|
||||
|
||||
/** A match N-ZAP's team has reported but the other has not confirmed, so it is the
|
||||
* other team's to report and N-ZAP's group is free to queue again. His side is
|
||||
* Alliance Rogue's lineup, so the match is one of a team against a pickup group. */
|
||||
async function seedNzapReportedMatch(users: SeededUsers, teams: SeededTeams) {
|
||||
const opponentIds = users.crowdIds.slice(-88, -84);
|
||||
|
||||
const match = await SQMatchFactory.create(
|
||||
{
|
||||
alphaUserIds: allianceRogueLineup(teams),
|
||||
bravoUserIds: opponentIds,
|
||||
isMatchmade: true,
|
||||
},
|
||||
{ isReported: true, createdAt: sub(new Date(), { hours: 1 }) },
|
||||
);
|
||||
|
||||
invariant(
|
||||
match.id === NZAP_MATCH_ID,
|
||||
`N-ZAP's match was created on id ${match.id}, not ${NZAP_MATCH_ID}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** One canceled match of each form the cancel reports take, so that the staff-only
|
||||
* views have every one of them to show: the two teams pointing at the same player,
|
||||
* at different ones, and a match staff canceled without either team's account of it. */
|
||||
async function seedNzapCanceledMatches(users: SeededUsers, teams: SeededTeams) {
|
||||
const [nzapId, ...teammateIds] = allianceRogueLineup(teams);
|
||||
const opponentIds = users.crowdIds.slice(-104, -88);
|
||||
const opponentGroup = (nth: number) =>
|
||||
opponentIds.slice(nth * FULL_GROUP_SIZE, (nth + 1) * FULL_GROUP_SIZE);
|
||||
|
||||
await SQMatchFactory.create(
|
||||
{
|
||||
alphaUserIds: [nzapId, ...teammateIds],
|
||||
bravoUserIds: opponentGroup(0),
|
||||
isMatchmade: true,
|
||||
},
|
||||
{
|
||||
cancel: {
|
||||
requested: {
|
||||
reason:
|
||||
"Their player never came back to the lobby after the second map. Waited 15 minutes and then gave up.",
|
||||
nominatedUserIds: [opponentGroup(0)[2]],
|
||||
},
|
||||
accepted: {
|
||||
reason:
|
||||
"Our teammate's console crashed and he couldn't get back online. Sorry for wasting everyone's time.",
|
||||
nominatedUserIds: [opponentGroup(0)[2]],
|
||||
},
|
||||
},
|
||||
createdAt: sub(new Date(), { days: 2, hours: 4 }),
|
||||
},
|
||||
);
|
||||
|
||||
await SQMatchFactory.create(
|
||||
{
|
||||
alphaUserIds: opponentGroup(1),
|
||||
bravoUserIds: [nzapId, ...teammateIds],
|
||||
isMatchmade: true,
|
||||
},
|
||||
{
|
||||
cancel: {
|
||||
requested: {
|
||||
reason: "Opponent left the lobby after going down 0-2.",
|
||||
nominatedUserIds: [nzapId],
|
||||
},
|
||||
accepted: {
|
||||
reason:
|
||||
"Power outage on my end, nothing intentional. They were flaming in chat the whole set before that.",
|
||||
nominatedUserIds: [opponentGroup(1)[0], opponentGroup(1)[3]],
|
||||
},
|
||||
},
|
||||
createdAt: sub(new Date(), { days: 9, hours: 11 }),
|
||||
},
|
||||
);
|
||||
|
||||
// a teammate owns the group, so that the report against N-ZAP is his own team's
|
||||
await SQMatchFactory.create(
|
||||
{
|
||||
alphaUserIds: opponentGroup(2),
|
||||
bravoUserIds: [...teammateIds, nzapId],
|
||||
isMatchmade: true,
|
||||
},
|
||||
{
|
||||
cancel: {
|
||||
requested: {
|
||||
reason: maxLengthCancelReason(),
|
||||
nominatedUserIds: [nzapId, opponentGroup(2)[1]],
|
||||
},
|
||||
accepted: {
|
||||
reason: "N-ZAP had to leave for work mid-set, our bad.",
|
||||
nominatedUserIds: [nzapId],
|
||||
},
|
||||
},
|
||||
createdAt: sub(new Date(), { days: 20, hours: 2 }),
|
||||
},
|
||||
);
|
||||
|
||||
await SQMatchFactory.create(
|
||||
{
|
||||
alphaUserIds: [nzapId, ...teammateIds],
|
||||
bravoUserIds: opponentGroup(3),
|
||||
isMatchmade: true,
|
||||
},
|
||||
{
|
||||
canceledByStaffUserId: users.staffId,
|
||||
createdAt: sub(new Date(), { days: 35, hours: 7 }),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** A reason at the exact max length, for how a wall of text lays out. */
|
||||
function maxLengthCancelReason() {
|
||||
return faker.lorem
|
||||
.paragraphs(5)
|
||||
.replaceAll("\n", " ")
|
||||
.slice(0, SENDOUQ.CANCEL_REASON_MAX_LENGTH);
|
||||
}
|
||||
|
||||
function allianceRogueLineup(teams: SeededTeams) {
|
||||
const allianceRogue = teams.squads.find(
|
||||
(squad) => squad.teamId === teams.allianceRogueId,
|
||||
);
|
||||
invariant(allianceRogue, "Alliance Rogue has no full lineup");
|
||||
|
||||
return allianceRogue.memberUserIds;
|
||||
}
|
||||
|
||||
/** Fixed team lineups playing together repeatedly, so their identifier skills reach
|
||||
* the match count the team leaderboard requires. */
|
||||
async function seedSquadMatches(teams: SeededTeams) {
|
||||
const squads = teams.squads.slice(0, SQUAD_COUNT);
|
||||
|
||||
for (let i = 0; i < SQUAD_MATCH_COUNT; i++) {
|
||||
const [alpha, bravo] = faker.helpers.arrayElements(squads, 2);
|
||||
|
||||
await SQMatchFactory.create(
|
||||
{
|
||||
alphaUserIds: alpha.memberUserIds,
|
||||
bravoUserIds: bravo.memberUserIds,
|
||||
},
|
||||
{
|
||||
isConcluded: true,
|
||||
createdAt: sub(new Date(), {
|
||||
days: faker.number.int({ min: 0, max: 45 }),
|
||||
hours: faker.number.int({ min: 0, max: 23 }),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function seedConcludedMatch(playerIds: number[], createdAt: Date) {
|
||||
const players = faker.helpers.arrayElements(playerIds, 8);
|
||||
|
||||
const match = await SQMatchFactory.create(
|
||||
{
|
||||
alphaUserIds: players.slice(0, 4),
|
||||
bravoUserIds: players.slice(4),
|
||||
},
|
||||
{ isConcluded: true, createdAt, confirmedAt: createdAt },
|
||||
);
|
||||
|
||||
if (faker.number.float(1) < 0.7) {
|
||||
// the first four maps are always played, whoever won
|
||||
await SQReportedWeaponFactory.createMany(
|
||||
players.length * REPORTED_MAP_COUNT,
|
||||
(i) => ({
|
||||
groupMatchId: match.id,
|
||||
mapIndex: Math.floor(i / players.length),
|
||||
userId: players[i % players.length],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
async function seedLookingGroups(users: SeededUsers) {
|
||||
// the tail of the crowd is free of tournament rosters, so these read as their own scene
|
||||
const availableUserIds = [users.nzapId, ...users.crowdIds.slice(-80)];
|
||||
|
||||
const groupIds: number[] = [];
|
||||
for (let i = 0; i < LOOKING_GROUP_COUNT; i++) {
|
||||
const memberCount =
|
||||
i === 0 ? 4 : faker.helpers.arrayElement([1, 1, 2, 3, 4]);
|
||||
const memberUserIds = availableUserIds.splice(0, memberCount);
|
||||
|
||||
const group = await SQGroupFactory.create(
|
||||
{ memberUserIds },
|
||||
{
|
||||
likedByGroupIds:
|
||||
groupIds.length > 1
|
||||
? faker.helpers.arrayElements(groupIds, { min: 0, max: 2 })
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
groupIds.push(group.id);
|
||||
}
|
||||
}
|
||||
81
apps/web-react/app/db/seed/dev/teams.ts
Normal file
81
apps/web-react/app/db/seed/dev/teams.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { faker } from "../core/faker";
|
||||
import * as showcaseNames from "../core/showcaseNames";
|
||||
import * as TeamFactory from "../factories/TeamFactory";
|
||||
import type { SeededUsers } from "./users";
|
||||
|
||||
const TEAM_COUNT = 40;
|
||||
const SECONDARY_TEAM_COUNT = 10;
|
||||
|
||||
export type SeededTeams = {
|
||||
allianceRogueId: number;
|
||||
ids: number[];
|
||||
/** Four members of a shared team, e.g. a lineup for the SQ team leaderboard. */
|
||||
squads: Array<{ teamId: number; name: string; memberUserIds: number[] }>;
|
||||
};
|
||||
|
||||
export async function seedTeams(users: SeededUsers): Promise<SeededTeams> {
|
||||
const memberPool = [...users.showcaseIds, ...users.crowdIds];
|
||||
let nextMember = 0;
|
||||
const takeMembers = (count: number) => {
|
||||
const members = memberPool.slice(nextMember, nextMember + count);
|
||||
nextMember += count;
|
||||
|
||||
return members;
|
||||
};
|
||||
|
||||
const allianceRogue = await TeamFactory.create(
|
||||
{
|
||||
name: "Alliance Rogue",
|
||||
memberUserIds: [users.nzapId, ...takeMembers(4)],
|
||||
},
|
||||
{ avatarUrl: "alliance-rogue.png" },
|
||||
);
|
||||
|
||||
const ids: number[] = [allianceRogue.id];
|
||||
const squads: SeededTeams["squads"] = [
|
||||
{
|
||||
teamId: allianceRogue.id,
|
||||
name: allianceRogue.name,
|
||||
memberUserIds: allianceRogue.memberUserIds.slice(0, 4),
|
||||
},
|
||||
];
|
||||
for (let i = 1; i < TEAM_COUNT; i++) {
|
||||
const memberCount = faker.helpers.arrayElement([
|
||||
1, 2, 3, 4, 4, 4, 4, 5, 5, 5, 6, 7, 8,
|
||||
]);
|
||||
const memberUserIds = takeMembers(memberCount);
|
||||
|
||||
const team = await TeamFactory.create(
|
||||
{
|
||||
name: i === 1 ? "Team Olive" : showcaseNames.teamName(),
|
||||
memberUserIds,
|
||||
},
|
||||
i === 1 || faker.number.float(1) < 0.3 ? { hasAvatar: true } : undefined,
|
||||
);
|
||||
|
||||
ids.push(team.id);
|
||||
if (memberCount >= 4) {
|
||||
squads.push({
|
||||
teamId: team.id,
|
||||
name: team.name,
|
||||
memberUserIds: memberUserIds.slice(0, 4),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// showcase users double as members of a secondary team; disjoint chunks so
|
||||
// nobody exceeds the two-team limit
|
||||
for (let i = 0; i < SECONDARY_TEAM_COUNT; i++) {
|
||||
const memberUserIds = users.showcaseIds.slice(i * 4, i * 4 + 4);
|
||||
|
||||
const team = await TeamFactory.create({
|
||||
name: showcaseNames.teamName(),
|
||||
isMainTeam: false,
|
||||
memberUserIds,
|
||||
});
|
||||
|
||||
ids.push(team.id);
|
||||
}
|
||||
|
||||
return { allianceRogueId: allianceRogue.id, ids, squads };
|
||||
}
|
||||
615
apps/web-react/app/db/seed/dev/tournaments.ts
Normal file
615
apps/web-react/app/db/seed/dev/tournaments.ts
Normal file
@@ -0,0 +1,615 @@
|
||||
import { sub } from "date-fns";
|
||||
import type { TournamentSettings } from "~/db/tables-json";
|
||||
import { MapPool } from "~/features/map-list-generator/core/map-pool";
|
||||
import { BANNED_MAPS } from "~/features/match-profile/banned-maps";
|
||||
import type { TournamentTierNumber } from "~/features/tournament/core/tiering";
|
||||
import { rankedModesShort } from "~/modules/in-game-lists/modes";
|
||||
import { stageIds } from "~/modules/in-game-lists/stage-ids";
|
||||
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import { faker, unique } from "../core/faker";
|
||||
import * as showcaseNames from "../core/showcaseNames";
|
||||
import * as ImageFactory from "../factories/ImageFactory";
|
||||
import * as SavedCalendarEventFactory from "../factories/SavedCalendarEventFactory";
|
||||
import * as TournamentFactory from "../factories/TournamentFactory";
|
||||
import * as TournamentLFGTeamFactory from "../factories/TournamentLFGTeamFactory";
|
||||
import * as TournamentStreamerFactory from "../factories/TournamentStreamerFactory";
|
||||
import * as TournamentTeamFactory from "../factories/TournamentTeamFactory";
|
||||
import type { SeededBadges } from "./badges";
|
||||
import type { SeededOrganization } from "./organizations";
|
||||
import type { SeededTeams } from "./teams";
|
||||
import type { SeededTrophies } from "./trophies";
|
||||
import type { SeededUsers } from "./users";
|
||||
|
||||
/** Series the played-out tournaments of the past are named off. The four the seed
|
||||
* puts in a state worth opening are named off a series of their own. */
|
||||
const TOURNAMENT_NAME_STEMS = [
|
||||
{ name: "PICNIC", avatarFileName: "picnic.png" },
|
||||
{ name: "The Depths", avatarFileName: "the-depths.png" },
|
||||
{ name: "Leagues Under The Ink", avatarFileName: "luti.png" },
|
||||
];
|
||||
|
||||
const HISTORICAL_COUNT = 5;
|
||||
/** Showcase users seeded into every played tournament, so their results paginate. */
|
||||
const CORE_PLAYER_COUNT = 8;
|
||||
/** Share of a tournament's teams that register as one of the site's teams, the rest
|
||||
* being pickups put together for the tournament. */
|
||||
const REGISTERED_TEAM_SHARE = 0.4;
|
||||
/** Solo players looking for a team in a tournament whose registration has closed. */
|
||||
const SUB_COUNT = 7;
|
||||
|
||||
/** One team's registration: the site team it registers as, when it is one of them. */
|
||||
type Roster = {
|
||||
teamId: number | null;
|
||||
name: string;
|
||||
memberUserIds: number[];
|
||||
};
|
||||
|
||||
type Progression = TournamentSettings["bracketProgression"];
|
||||
|
||||
const DOUBLE_ELIMINATION: Progression = [
|
||||
{
|
||||
type: "double_elimination",
|
||||
name: "Main Bracket",
|
||||
requiresCheckIn: false,
|
||||
settings: {},
|
||||
},
|
||||
];
|
||||
|
||||
const DOUBLE_ELIMINATION_WITH_UNDERGROUND: Progression = [
|
||||
...DOUBLE_ELIMINATION,
|
||||
{
|
||||
type: "single_elimination",
|
||||
name: "Underground Bracket",
|
||||
requiresCheckIn: false,
|
||||
settings: {},
|
||||
sources: [{ bracketIdx: 0, placements: [-1, -2] }],
|
||||
},
|
||||
];
|
||||
|
||||
const SINGLE_ELIMINATION: Progression = [
|
||||
{
|
||||
type: "single_elimination",
|
||||
name: "Bracket",
|
||||
requiresCheckIn: false,
|
||||
settings: { thirdPlaceMatch: true },
|
||||
},
|
||||
];
|
||||
|
||||
const ROUND_ROBIN_TO_SINGLE_ELIMINATION: Progression = [
|
||||
{
|
||||
type: "round_robin",
|
||||
name: "Groups Stage",
|
||||
requiresCheckIn: false,
|
||||
settings: {},
|
||||
},
|
||||
{
|
||||
type: "single_elimination",
|
||||
name: "Final Stage",
|
||||
requiresCheckIn: false,
|
||||
settings: {},
|
||||
sources: [{ bracketIdx: 0, placements: [1, 2] }],
|
||||
},
|
||||
];
|
||||
|
||||
const ROUND_ROBIN_TO_TOP_CUT_AND_LOWER: Progression = [
|
||||
...ROUND_ROBIN_TO_SINGLE_ELIMINATION,
|
||||
{
|
||||
type: "single_elimination",
|
||||
name: "Lower Bracket",
|
||||
requiresCheckIn: false,
|
||||
settings: {},
|
||||
sources: [{ bracketIdx: 0, placements: [3, 4] }],
|
||||
},
|
||||
];
|
||||
|
||||
const SWISS_TO_SINGLE_ELIMINATION: Progression = [
|
||||
{
|
||||
type: "swiss",
|
||||
name: "Swiss",
|
||||
requiresCheckIn: false,
|
||||
settings: { groupCount: 1, roundCount: 4 },
|
||||
},
|
||||
{
|
||||
type: "single_elimination",
|
||||
name: "Top Cut",
|
||||
requiresCheckIn: false,
|
||||
settings: {},
|
||||
sources: [{ bracketIdx: 0, placements: [1, 2, 3, 4] }],
|
||||
},
|
||||
];
|
||||
|
||||
export type SeededTournaments = {
|
||||
/** The one with registration still open, which the notifications are about. */
|
||||
regOpen: { id: number; name: string };
|
||||
/** Teams N-ZAP played on in the tournaments that were played to the end. */
|
||||
nzapTeamIds: number[];
|
||||
};
|
||||
|
||||
export async function seedTournaments({
|
||||
users,
|
||||
organizations,
|
||||
badges,
|
||||
teams,
|
||||
trophies,
|
||||
}: {
|
||||
users: SeededUsers;
|
||||
organizations: SeededOrganization[];
|
||||
badges: SeededBadges;
|
||||
teams: SeededTeams;
|
||||
trophies: SeededTrophies;
|
||||
}): Promise<SeededTournaments> {
|
||||
const rosters = rosterBuilder(users, teams);
|
||||
|
||||
const inTheZone = await seedInTheZone({
|
||||
users,
|
||||
organizations,
|
||||
rosters,
|
||||
trophies,
|
||||
});
|
||||
await seedPaddlingPool({ users, organizations, rosters });
|
||||
await seedLowInk({ users, organizations, rosters });
|
||||
await seedSwimOrSink({ users, organizations, rosters });
|
||||
|
||||
const nzapTeamIds = await seedHistoricalTournaments({
|
||||
users,
|
||||
organizations,
|
||||
badges,
|
||||
rosters,
|
||||
trophies,
|
||||
});
|
||||
|
||||
return { regOpen: inTheZone, nzapTeamIds };
|
||||
}
|
||||
|
||||
type Ctx = {
|
||||
users: SeededUsers;
|
||||
organizations: SeededOrganization[];
|
||||
rosters: ReturnType<typeof rosterBuilder>;
|
||||
};
|
||||
|
||||
/** #1 double elim, TO maps — reg open and a couple of days out, so it has both
|
||||
* registered teams (some of them still short of a full roster) and LFG teams. */
|
||||
async function seedInTheZone({
|
||||
users,
|
||||
organizations,
|
||||
rosters,
|
||||
trophies,
|
||||
}: Ctx & { trophies: SeededTrophies }) {
|
||||
const name = nameFor("In The Zone");
|
||||
|
||||
const tournament = await TournamentFactory.create({
|
||||
name,
|
||||
authorId: users.adminId,
|
||||
organizationId: organizations[0]?.id,
|
||||
avatarFileName: "in-the-zone.png",
|
||||
startTimes: [dateToDatabaseTimestamp(daysFromNow(2))],
|
||||
mapPickingStyle: "TO",
|
||||
mapPoolMaps: toSetMapPool(),
|
||||
bracketProgression: DOUBLE_ELIMINATION,
|
||||
enableSubs: true,
|
||||
trophyId: trophies.ids[0],
|
||||
});
|
||||
|
||||
const teamRosters = rosters.take({
|
||||
teamCount: 10,
|
||||
teamSize: 4,
|
||||
pinned: [{ teamIdx: 0, userId: users.adminId }],
|
||||
});
|
||||
|
||||
for (const [i, roster] of teamRosters.entries()) {
|
||||
await TournamentTeamFactory.create({
|
||||
tournamentId: tournament.id,
|
||||
team: fakeTeamProfile(roster),
|
||||
// not every registered roster is full while reg is still open
|
||||
memberUserIds:
|
||||
i % 3 === 2 ? roster.memberUserIds.slice(0, 3) : roster.memberUserIds,
|
||||
hasAvatar: roster.teamId === null && i % 4 === 0,
|
||||
});
|
||||
}
|
||||
|
||||
await seedTournamentExtras(tournament.id, users);
|
||||
|
||||
return { id: tournament.id, name };
|
||||
}
|
||||
|
||||
/** #2 double elim with an underground bracket, AUTO_SZ, ranked — bracket started,
|
||||
* not a single set reported yet. N-ZAP is seeded past the byes of the first round,
|
||||
* so he has a match of his own going on. */
|
||||
async function seedPaddlingPool({ users, rosters }: Ctx) {
|
||||
const tournament = await TournamentFactory.create({
|
||||
name: nameFor("Paddling Pool"),
|
||||
authorId: users.adminId,
|
||||
avatarFileName: "paddling-pool.png",
|
||||
startTimes: [dateToDatabaseTimestamp(hoursAgo(1))],
|
||||
mapPickingStyle: "AUTO_SZ",
|
||||
bracketProgression: DOUBLE_ELIMINATION_WITH_UNDERGROUND,
|
||||
isRanked: true,
|
||||
});
|
||||
|
||||
const teamRosters = rosters.take({
|
||||
teamCount: 12,
|
||||
teamSize: 4,
|
||||
pinned: [{ teamIdx: 4, userId: users.nzapId }],
|
||||
});
|
||||
|
||||
await registerTeams({
|
||||
tournamentId: tournament.id,
|
||||
rosters: teamRosters,
|
||||
isCheckedIn: true,
|
||||
mapPool: () => counterpickMapPool("AUTO_SZ"),
|
||||
});
|
||||
|
||||
await TournamentFactory.startBracket(tournament.id);
|
||||
await seedSubs(tournament.id, users);
|
||||
}
|
||||
|
||||
/** #3 swiss → SE, TO maps — swiss played to the end, the top cut waiting to be started. */
|
||||
async function seedLowInk({ users, rosters }: Ctx) {
|
||||
const tournament = await TournamentFactory.create({
|
||||
name: nameFor("Low Ink"),
|
||||
authorId: users.adminId,
|
||||
startTimes: [dateToDatabaseTimestamp(hoursAgo(4))],
|
||||
mapPickingStyle: "TO",
|
||||
mapPoolMaps: toSetMapPool(),
|
||||
bracketProgression: SWISS_TO_SINGLE_ELIMINATION,
|
||||
swissGroupCount: 1,
|
||||
swissRoundCount: 4,
|
||||
});
|
||||
|
||||
const teamRosters = rosters.take({
|
||||
teamCount: 8,
|
||||
teamSize: 4,
|
||||
pinned: [{ teamIdx: 0, userId: users.nzapId }],
|
||||
});
|
||||
|
||||
await registerTeams({
|
||||
tournamentId: tournament.id,
|
||||
rosters: teamRosters,
|
||||
isCheckedIn: true,
|
||||
});
|
||||
|
||||
await TournamentFactory.playOut(tournament.id, 0);
|
||||
}
|
||||
|
||||
/** #4 round robin → SE, TO maps — everybody checked in, first bracket not started.
|
||||
* The one upcoming tournament N-ZAP is not registered in, so it is his saved one. */
|
||||
async function seedSwimOrSink({ users, rosters }: Ctx) {
|
||||
const tournament = await TournamentFactory.create({
|
||||
name: nameFor("Swim or Sink"),
|
||||
authorId: users.adminId,
|
||||
avatarFileName: "swim-or-sink.png",
|
||||
startTimes: [dateToDatabaseTimestamp(hoursAgo(1))],
|
||||
mapPickingStyle: "TO",
|
||||
mapPoolMaps: toSetMapPool(),
|
||||
bracketProgression: ROUND_ROBIN_TO_SINGLE_ELIMINATION,
|
||||
teamsPerGroup: 4,
|
||||
});
|
||||
|
||||
const teamRosters = rosters.take({ teamCount: 12, teamSize: 4 });
|
||||
|
||||
await registerTeams({
|
||||
tournamentId: tournament.id,
|
||||
rosters: teamRosters,
|
||||
isCheckedIn: true,
|
||||
});
|
||||
|
||||
await SavedCalendarEventFactory.create({
|
||||
userId: users.nzapId,
|
||||
tournamentId: tournament.id,
|
||||
});
|
||||
}
|
||||
|
||||
async function seedHistoricalTournaments({
|
||||
users,
|
||||
badges,
|
||||
rosters,
|
||||
trophies,
|
||||
}: Ctx & { badges: SeededBadges; trophies: SeededTrophies }) {
|
||||
const nzapTeamIds: number[] = [];
|
||||
const seriesLogoImgIds = new Map<string, number>();
|
||||
|
||||
for (let i = 0; i < HISTORICAL_COUNT; i++) {
|
||||
const progression = faker.helpers.weightedArrayElement([
|
||||
{ value: DOUBLE_ELIMINATION, weight: 5 },
|
||||
{ value: ROUND_ROBIN_TO_SINGLE_ELIMINATION, weight: 3 },
|
||||
{ value: SINGLE_ELIMINATION, weight: 1 },
|
||||
{ value: DOUBLE_ELIMINATION_WITH_UNDERGROUND, weight: 1 },
|
||||
{ value: ROUND_ROBIN_TO_TOP_CUT_AND_LOWER, weight: 1 },
|
||||
]);
|
||||
// recent ones ranked and within the front page's week-long results window
|
||||
const isRecent = i < 3;
|
||||
const startsAt = isRecent
|
||||
? sub(new Date(), { days: 1 + i, hours: 3 })
|
||||
: sub(new Date(), { months: 1 + (i % 8), days: (i * 7) % 28 });
|
||||
const badgeId = i % 3 === 0 ? badges.ids[i % badges.ids.length] : undefined;
|
||||
const stem = TOURNAMENT_NAME_STEMS[i % TOURNAMENT_NAME_STEMS.length];
|
||||
const authorId = faker.helpers.arrayElement(users.showcaseIds);
|
||||
|
||||
const tournament = await TournamentFactory.create(
|
||||
{
|
||||
name: nameFor(stem.name),
|
||||
avatarImgId: await seriesLogoImgId(seriesLogoImgIds, stem, authorId),
|
||||
authorId,
|
||||
startTimes: [dateToDatabaseTimestamp(startsAt)],
|
||||
mapPickingStyle: isRecent ? "AUTO_SZ" : "AUTO_ALL",
|
||||
mapPoolMaps: isRecent ? undefined : tiebreakerMapPool(),
|
||||
bracketProgression: progression,
|
||||
teamsPerGroup: 4,
|
||||
isRanked: isRecent,
|
||||
badges: badgeId ? [badgeId] : [],
|
||||
trophyId: trophies.ids[i % trophies.ids.length],
|
||||
},
|
||||
{ tier: ((i % 3) + 1) as TournamentTierNumber },
|
||||
);
|
||||
|
||||
// on the top seed of the first one, so that a win of his is finalized, and
|
||||
// further down another, so his result list is not all first places
|
||||
const nzapRosterIdx = i === 0 ? 0 : i === 2 ? 5 : null;
|
||||
|
||||
const teamRosters = rosters.take({
|
||||
teamCount: 8,
|
||||
teamSize: 4,
|
||||
pinned:
|
||||
nzapRosterIdx !== null
|
||||
? [{ teamIdx: nzapRosterIdx, userId: users.nzapId }]
|
||||
: [],
|
||||
});
|
||||
|
||||
const teams = await registerTeams({
|
||||
tournamentId: tournament.id,
|
||||
rosters: teamRosters,
|
||||
isCheckedIn: true,
|
||||
registeredAt: sub(startsAt, { days: 2 }),
|
||||
mapPool: () => counterpickMapPool(isRecent ? "AUTO_SZ" : "AUTO_ALL"),
|
||||
});
|
||||
|
||||
if (nzapRosterIdx !== null) {
|
||||
nzapTeamIds.push(teams[nzapRosterIdx].id);
|
||||
}
|
||||
|
||||
await TournamentFactory.playOut(tournament.id, "all");
|
||||
}
|
||||
|
||||
return nzapTeamIds;
|
||||
}
|
||||
|
||||
/** Every edition of a series shares the one logo image of it, an image row not being
|
||||
* allowed the url of another. */
|
||||
async function seriesLogoImgId(
|
||||
imgIds: Map<string, number>,
|
||||
stem: (typeof TOURNAMENT_NAME_STEMS)[number],
|
||||
authorId: number,
|
||||
) {
|
||||
const existing = imgIds.get(stem.name);
|
||||
if (existing) return existing;
|
||||
|
||||
const image = await ImageFactory.create(
|
||||
{ submitterUserId: authorId, url: stem.avatarFileName },
|
||||
{ isValidated: true },
|
||||
);
|
||||
imgIds.set(stem.name, image.id);
|
||||
|
||||
return image.id;
|
||||
}
|
||||
|
||||
/** Solo players looking to sub in a tournament whose registration has closed, the
|
||||
* admin among them so that the state of having posted is one of the two profiles'.
|
||||
* Drawn from the tail of the crowd, which no tournament roster reaches. */
|
||||
async function seedSubs(tournamentId: number, users: SeededUsers) {
|
||||
const userIds = [
|
||||
users.adminId,
|
||||
...users.crowdIds.slice(300, 300 + SUB_COUNT - 1),
|
||||
];
|
||||
|
||||
for (const [i, userId] of userIds.entries()) {
|
||||
await TournamentLFGTeamFactory.create({
|
||||
tournamentId,
|
||||
userId,
|
||||
isStayAsSub: true,
|
||||
lfgNote: i % 3 === 0 ? undefined : showcaseNames.postText(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function seedTournamentExtras(tournamentId: number, users: SeededUsers) {
|
||||
for (const twitchAccount of ["sendou", "nzap_stream"]) {
|
||||
await TournamentStreamerFactory.create({ tournamentId, twitchAccount });
|
||||
}
|
||||
|
||||
const lfgUserIds = [users.nzapId, ...users.showcaseIds.slice(90, 95)];
|
||||
|
||||
const lfgTeamIds: number[] = [];
|
||||
for (const [i, userId] of lfgUserIds.entries()) {
|
||||
const team = await TournamentLFGTeamFactory.create(
|
||||
{ tournamentId, userId },
|
||||
{ likedTeamIds: lfgTeamIds.slice(0, i % 3) },
|
||||
);
|
||||
lfgTeamIds.push(team.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function registerTeams({
|
||||
tournamentId,
|
||||
rosters,
|
||||
isCheckedIn,
|
||||
registeredAt,
|
||||
mapPool,
|
||||
}: {
|
||||
tournamentId: number;
|
||||
rosters: Roster[];
|
||||
isCheckedIn?: boolean;
|
||||
registeredAt?: Date;
|
||||
mapPool?: () => MapPool;
|
||||
}) {
|
||||
const teams = [];
|
||||
for (const [i, roster] of rosters.entries()) {
|
||||
teams.push(
|
||||
await TournamentTeamFactory.create(
|
||||
{
|
||||
tournamentId,
|
||||
team: fakeTeamProfile(roster),
|
||||
memberUserIds: roster.memberUserIds,
|
||||
mapPool: mapPool?.(),
|
||||
registeredAt,
|
||||
// a team of the site shows its own logo when the registration has none
|
||||
hasAvatar: roster.teamId === null && i % 5 === 0,
|
||||
},
|
||||
{ isCheckedIn },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return teams;
|
||||
}
|
||||
|
||||
function rosterBuilder(users: SeededUsers, teams: SeededTeams) {
|
||||
const corePlayers = users.showcaseIds.slice(0, CORE_PLAYER_COUNT);
|
||||
const pool = [
|
||||
...users.showcaseIds.slice(CORE_PLAYER_COUNT),
|
||||
...users.crowdIds.slice(0, 300),
|
||||
];
|
||||
|
||||
return {
|
||||
/** Rosters for one tournament: some of the site's teams registering as
|
||||
* themselves, core players spread over the rest, and the remaining seats drawn
|
||||
* without replacement within the tournament. A `pinned` user is added to a
|
||||
* roster of their own as its owner, and kept out of everybody else's. */
|
||||
take({
|
||||
teamCount,
|
||||
teamSize,
|
||||
pinned = [],
|
||||
}: {
|
||||
teamCount: number;
|
||||
teamSize: number;
|
||||
pinned?: Array<{ teamIdx: number; userId: number }>;
|
||||
}): Roster[] {
|
||||
const pinnedUserIds = new Set(pinned.map((pin) => pin.userId));
|
||||
const registering = faker.helpers
|
||||
.shuffle(
|
||||
teams.squads.filter((squad) =>
|
||||
squad.memberUserIds.every((id) => !pinnedUserIds.has(id)),
|
||||
),
|
||||
)
|
||||
.slice(0, Math.round(teamCount * REGISTERED_TEAM_SHARE));
|
||||
|
||||
// a tournament can not have two teams of the same name
|
||||
const takenNames = new Set(registering.map((squad) => squad.name));
|
||||
|
||||
const takenUserIds = new Set([
|
||||
...pinnedUserIds,
|
||||
...registering.flatMap((squad) => squad.memberUserIds),
|
||||
]);
|
||||
const isFree = (userId: number) => !takenUserIds.has(userId);
|
||||
|
||||
const shuffled = faker.helpers.shuffle(pool.filter(isFree));
|
||||
const freeCorePlayers = corePlayers.filter(isFree);
|
||||
|
||||
// the teams of the site take the first team slots a pin does not want
|
||||
const pinnedIdxs = new Set(pinned.map((pin) => pin.teamIdx));
|
||||
const registeringIdxs = Array.from({ length: teamCount }, (_, i) => i)
|
||||
.filter((i) => !pinnedIdxs.has(i))
|
||||
.slice(0, registering.length);
|
||||
|
||||
return Array.from({ length: teamCount }, (_, i) => {
|
||||
const registeringIdx = registeringIdxs.indexOf(i);
|
||||
if (registeringIdx !== -1) {
|
||||
const squad = registering[registeringIdx];
|
||||
|
||||
return {
|
||||
teamId: squad.teamId,
|
||||
name: squad.name,
|
||||
memberUserIds: squad.memberUserIds.slice(0, teamSize),
|
||||
};
|
||||
}
|
||||
|
||||
const memberUserIds: number[] = [];
|
||||
if (teamSize >= 2 && freeCorePlayers.length > 0) {
|
||||
memberUserIds.push(freeCorePlayers.shift()!);
|
||||
}
|
||||
|
||||
while (memberUserIds.length < teamSize) {
|
||||
memberUserIds.push(shuffled.pop()!);
|
||||
}
|
||||
|
||||
const pin = pinned.find((pin) => pin.teamIdx === i);
|
||||
if (pin) {
|
||||
memberUserIds.unshift(pin.userId);
|
||||
}
|
||||
|
||||
return {
|
||||
teamId: null,
|
||||
name: pickupTeamName(takenNames),
|
||||
memberUserIds,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** A name for a team put together for one tournament, taken by no other team of it. */
|
||||
function pickupTeamName(takenNames: Set<string>) {
|
||||
let name = unique(() => showcaseNames.teamName());
|
||||
while (takenNames.has(name)) {
|
||||
name = unique(() => showcaseNames.teamName());
|
||||
}
|
||||
takenNames.add(name);
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
function fakeTeamProfile(roster: Roster) {
|
||||
return {
|
||||
name: roster.name,
|
||||
prefersNotToHost: faker.number.float(1) < 0.2 ? (1 as const) : (0 as const),
|
||||
teamId: roster.teamId,
|
||||
};
|
||||
}
|
||||
|
||||
/** Series name and the edition of it this tournament is, as they are named. */
|
||||
function nameFor(stem: string) {
|
||||
return `${stem} ${faker.number.int({ min: 2, max: 120 })}`;
|
||||
}
|
||||
|
||||
function toSetMapPool() {
|
||||
return mapsPerMode(7);
|
||||
}
|
||||
|
||||
function tiebreakerMapPool() {
|
||||
return mapsPerMode(1);
|
||||
}
|
||||
|
||||
function mapsPerMode(count: number) {
|
||||
return rankedModesShort.flatMap((mode) =>
|
||||
legalStages(mode)
|
||||
.slice(0, count)
|
||||
.map((stageId) => ({ mode, stageId })),
|
||||
);
|
||||
}
|
||||
|
||||
function counterpickMapPool(style: "AUTO_SZ" | "AUTO_ALL") {
|
||||
const pairs =
|
||||
style === "AUTO_SZ"
|
||||
? faker.helpers
|
||||
.arrayElements(legalStages("SZ"), 6)
|
||||
.map((stageId) => ({ mode: "SZ" as const, stageId }))
|
||||
: rankedModesShort.flatMap((mode) =>
|
||||
faker.helpers
|
||||
.arrayElements(legalStages(mode), 2)
|
||||
.map((stageId) => ({ mode, stageId })),
|
||||
);
|
||||
|
||||
return new MapPool(pairs);
|
||||
}
|
||||
|
||||
function legalStages(mode: ModeShort): StageId[] {
|
||||
return stageIds.filter((stageId) => !BANNED_MAPS[mode].includes(stageId));
|
||||
}
|
||||
|
||||
function daysFromNow(days: number) {
|
||||
return new Date(Date.now() + days * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
function hoursAgo(hours: number) {
|
||||
return new Date(Date.now() - hours * 60 * 60 * 1000);
|
||||
}
|
||||
125
apps/web-react/app/db/seed/dev/trophies.ts
Normal file
125
apps/web-react/app/db/seed/dev/trophies.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import * as TrophyRepository from "~/features/trophies/TrophyRepository.server";
|
||||
import {
|
||||
SUPPORTER_TROPHY_CODE,
|
||||
XP_TROPHY_CODE_PREFIX,
|
||||
} from "~/features/trophies/trophies-constants";
|
||||
import { faker } from "../core/faker";
|
||||
import trophies from "../data/trophies.json";
|
||||
import * as TrophyFactory from "../factories/TrophyFactory";
|
||||
import type { SeededOrganization } from "./organizations";
|
||||
import type { SeededUsers } from "./users";
|
||||
|
||||
const PENDING_COUNT = 5;
|
||||
const PARTIALLY_APPROVED_COUNT = 2;
|
||||
const ACCEPTED_COUNT = 3;
|
||||
const DECLINED_COUNT = 3;
|
||||
|
||||
export type SeededTrophies = {
|
||||
/** Ids of the trophies tournaments can be given as a prize. */
|
||||
ids: number[];
|
||||
};
|
||||
|
||||
export async function seedTrophies({
|
||||
users,
|
||||
organizations,
|
||||
}: {
|
||||
users: SeededUsers;
|
||||
organizations: SeededOrganization[];
|
||||
}): Promise<SeededTrophies> {
|
||||
const ids: number[] = [];
|
||||
for (const [name, model] of Object.entries(trophies)) {
|
||||
const trophy = await TrophyFactory.create({
|
||||
name,
|
||||
model,
|
||||
organizationId: organizations[0]?.id ?? null,
|
||||
creatorId: users.adminId,
|
||||
managerId: users.nzapId,
|
||||
});
|
||||
|
||||
ids.push(trophy.id);
|
||||
}
|
||||
|
||||
await seedPendingTrophies({ users, organizations });
|
||||
|
||||
return { ids };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the trophies awarded off something other than a tournament win and hands
|
||||
* them to everybody eligible, as the nightly sync does. Runs last of the seed: who
|
||||
* is eligible follows from the patrons and X Rank placements seeded before it.
|
||||
*/
|
||||
export async function seedSpecialTrophies() {
|
||||
const models = TrophyFactory.MODELS;
|
||||
|
||||
await TrophyFactory.create({
|
||||
name: "Supporter",
|
||||
model: models[0],
|
||||
code: SUPPORTER_TROPHY_CODE,
|
||||
});
|
||||
await TrophyFactory.create({
|
||||
name: "3000 X Power",
|
||||
model: models[1],
|
||||
code: `${XP_TROPHY_CODE_PREFIX}3000`,
|
||||
});
|
||||
await TrophyFactory.create({
|
||||
name: "2600 X Power",
|
||||
model: models[2],
|
||||
code: `${XP_TROPHY_CODE_PREFIX}2600`,
|
||||
});
|
||||
|
||||
await TrophyRepository.syncSpecialTrophies();
|
||||
}
|
||||
|
||||
async function seedPendingTrophies({
|
||||
users,
|
||||
organizations,
|
||||
}: {
|
||||
users: SeededUsers;
|
||||
organizations: SeededOrganization[];
|
||||
}) {
|
||||
const organizationId = organizations[0].id;
|
||||
const submitterIds = users.showcaseIds.slice(0, 10);
|
||||
const submission = (index: number) => ({
|
||||
organizationId,
|
||||
submitterUserId: faker.helpers.arrayElement(submitterIds),
|
||||
description: faker.lorem.sentence(),
|
||||
name: `Pending trophy ${index + 1}`,
|
||||
});
|
||||
|
||||
await TrophyFactory.createManyPending(PENDING_COUNT, (index) =>
|
||||
submission(index),
|
||||
);
|
||||
|
||||
await TrophyFactory.createManyPending(
|
||||
PARTIALLY_APPROVED_COUNT,
|
||||
(index) => ({
|
||||
...submission(index),
|
||||
name: `Partially approved trophy ${index + 1}`,
|
||||
}),
|
||||
{ approverUserIds: [users.adminId] },
|
||||
);
|
||||
|
||||
await TrophyFactory.createManyPending(
|
||||
ACCEPTED_COUNT,
|
||||
(index) => ({
|
||||
...submission(index),
|
||||
name: `Accepted trophy ${index + 1}`,
|
||||
}),
|
||||
{ approverUserIds: [users.adminId, users.staffId, users.orgAdminId] },
|
||||
);
|
||||
|
||||
await TrophyFactory.createManyPending(
|
||||
DECLINED_COUNT,
|
||||
(index) => ({
|
||||
...submission(index),
|
||||
name: `Declined trophy ${index + 1}`,
|
||||
}),
|
||||
{
|
||||
declinedBy: {
|
||||
userId: users.adminId,
|
||||
reason: faker.lorem.sentence(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
362
apps/web-react/app/db/seed/dev/users.ts
Normal file
362
apps/web-react/app/db/seed/dev/users.ts
Normal file
@@ -0,0 +1,362 @@
|
||||
import type { UserMapModePreferences } from "~/db/tables-json";
|
||||
import { ADMIN_DISCORD_ID } from "~/features/admin/admin-constants";
|
||||
import { BANNED_MAPS } from "~/features/match-profile/banned-maps";
|
||||
import { AMOUNT_OF_MAPS_IN_POOL_PER_MODE } from "~/features/match-profile/match-profile-constants";
|
||||
import { LUTI_DIVS } from "~/features/scrims/scrims-constants";
|
||||
import { PRESET_COLORS } from "~/features/tier-list-maker/tier-list-maker-constants";
|
||||
import type { UnifiedLanguageCode } from "~/modules/i18n/config";
|
||||
import { modesShort } from "~/modules/in-game-lists/modes";
|
||||
import { stageIds } from "~/modules/in-game-lists/stage-ids";
|
||||
import {
|
||||
ADMIN_TEST_AVATAR,
|
||||
NZAP_TEST_AVATAR,
|
||||
NZAP_TEST_DISCORD_ID,
|
||||
STAFF_TEST_DISCORD_ID,
|
||||
} from "../constants";
|
||||
import { faker } from "../core/faker";
|
||||
import * as SplatoonFaker from "../core/SplatoonFaker";
|
||||
import * as showcaseNames from "../core/showcaseNames";
|
||||
import * as UserFactory from "../factories/UserFactory";
|
||||
|
||||
const SHOWCASE_COUNT = 100;
|
||||
const CROWD_COUNT = 396;
|
||||
|
||||
export type SeededUsers = {
|
||||
adminId: number;
|
||||
nzapId: number;
|
||||
orgAdminId: number;
|
||||
staffId: number;
|
||||
/** The curated first ~100, wired into everything later modules seed. */
|
||||
showcaseIds: number[];
|
||||
crowdIds: number[];
|
||||
/** Showcase users with the artist role, some with commissions open. */
|
||||
artistIds: number[];
|
||||
/** Showcase users whose profile can gain favorite badges without losing anything. */
|
||||
favoriteBadgeUserIds: number[];
|
||||
};
|
||||
|
||||
export async function seedUsers(): Promise<SeededUsers> {
|
||||
// the plainest of the two profiles: no supporter perks, so the old profile page
|
||||
const admin = await UserFactory.createAdmin(
|
||||
{
|
||||
discordId: ADMIN_DISCORD_ID,
|
||||
discordName: "Sendou",
|
||||
discordUniqueName: "sendou",
|
||||
discordAvatar: ADMIN_TEST_AVATAR,
|
||||
twitch: "Sendou",
|
||||
profile: {
|
||||
country: "FI",
|
||||
customUrl: "sendou",
|
||||
inGameName: "Sendou#1234",
|
||||
bio: showcaseNames.postText(),
|
||||
weapons: [{ weaponSplId: 200, isFavorite: 0 }],
|
||||
},
|
||||
friendCode: "0109-8080-3707",
|
||||
},
|
||||
{
|
||||
roles: ["VIDEO_ADDER", "TOURNAMENT_ORGANIZER", "ARTIST"],
|
||||
matchProfile: { mapModePreferences: fakePreferences(), vc: "YES" },
|
||||
},
|
||||
);
|
||||
|
||||
const nzap = await UserFactory.createRegular(
|
||||
{
|
||||
discordId: NZAP_TEST_DISCORD_ID,
|
||||
discordName: "N-ZAP",
|
||||
discordUniqueName: "nzap",
|
||||
discordAvatar: NZAP_TEST_AVATAR,
|
||||
twitch: "nzap_stream",
|
||||
youtubeId: "UCWbJLXByvsfQvTcR4HLPs5Q",
|
||||
bsky: "nzap.bsky.social",
|
||||
profile: {
|
||||
country: "SE",
|
||||
customUrl: "nzap",
|
||||
motionSens: 50,
|
||||
stickSens: 5,
|
||||
pronouns: JSON.stringify({ subject: "they", object: "them" }),
|
||||
inGameName: "N-ZAP#5678",
|
||||
bio: showcaseNames.maxLengthBio(),
|
||||
weapons: ([200, 1100, 2000, 4000] as const).map((weaponSplId) => ({
|
||||
weaponSplId,
|
||||
isFavorite: 0 as const,
|
||||
})),
|
||||
},
|
||||
friendCode: "1234-5678-9012",
|
||||
},
|
||||
{
|
||||
patronTier: 2,
|
||||
roles: ["VIDEO_ADDER", "TOURNAMENT_ORGANIZER", "ARTIST"],
|
||||
matchProfile: {
|
||||
mapModePreferences: fakePreferences(),
|
||||
vc: "YES",
|
||||
languages: ["en", "ja"],
|
||||
weaponPool: SplatoonFaker.mainWeapons(4).map((id) => ({
|
||||
id,
|
||||
isFavorite: false,
|
||||
})),
|
||||
},
|
||||
card: { shortBio: "Supporter of sendou.ink" },
|
||||
preferences: { newProfileEnabled: true },
|
||||
widgets: nzapWidgets(),
|
||||
},
|
||||
);
|
||||
|
||||
const orgAdmin = await UserFactory.createOrgAdmin(null, {
|
||||
roles: ["API_ACCESSER"],
|
||||
});
|
||||
|
||||
const showcase = await seedShowcaseUsers();
|
||||
const crowd = await UserFactory.createMany(CROWD_COUNT);
|
||||
for (const [i, user] of crowd.entries()) {
|
||||
if (i % 2 === 0) continue;
|
||||
|
||||
await UserFactory.grant(user.id, {
|
||||
matchProfile: {
|
||||
weaponPool: SplatoonFaker.mainWeapons(
|
||||
faker.helpers.arrayElement([1, 2, 3, 4]),
|
||||
).map((id) => ({ id, isFavorite: faker.number.float(1) < 0.3 })),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const staff = await UserFactory.createStaff({
|
||||
discordId: STAFF_TEST_DISCORD_ID,
|
||||
discordName: "Panda",
|
||||
});
|
||||
|
||||
return {
|
||||
adminId: admin.id,
|
||||
nzapId: nzap.id,
|
||||
orgAdminId: orgAdmin.id,
|
||||
staffId: staff.id,
|
||||
showcaseIds: showcase.ids,
|
||||
crowdIds: crowd.map((user) => user.id),
|
||||
artistIds: showcase.artistIds,
|
||||
favoriteBadgeUserIds: showcase.favoriteBadgeUserIds,
|
||||
};
|
||||
}
|
||||
|
||||
async function seedShowcaseUsers() {
|
||||
const ids: number[] = [];
|
||||
const artistIds: number[] = [];
|
||||
const favoriteBadgeUserIds: number[] = [];
|
||||
|
||||
for (const [i, customName] of showcaseNames.CUSTOM_NAMES.entries()) {
|
||||
const hasKanji = /[一-龯]/u.test(customName);
|
||||
|
||||
const user = await UserFactory.create(
|
||||
{
|
||||
profile: {
|
||||
customName,
|
||||
inGameName: hasKanji
|
||||
? showcaseNames.kanaInGameName()
|
||||
: SplatoonFaker.inGameName(),
|
||||
bio: i === 0 ? showcaseNames.maxLengthBio() : undefined,
|
||||
weapons: [],
|
||||
},
|
||||
},
|
||||
showcaseOptions(),
|
||||
);
|
||||
|
||||
ids.push(user.id);
|
||||
favoriteBadgeUserIds.push(user.id);
|
||||
}
|
||||
|
||||
const noProfileUser = await UserFactory.create({
|
||||
profile: null,
|
||||
friendCode: null,
|
||||
});
|
||||
ids.push(noProfileUser.id);
|
||||
|
||||
const maximalUser = await UserFactory.create(
|
||||
{
|
||||
profile: {
|
||||
customName: showcaseNames.customName(),
|
||||
customUrl: "maximal",
|
||||
country: "JP",
|
||||
bio: showcaseNames.maxLengthBio(),
|
||||
pronouns: JSON.stringify({ subject: "they", object: "them" }),
|
||||
motionSens: -25,
|
||||
stickSens: 10,
|
||||
inGameName: showcaseNames.kanaInGameName(),
|
||||
weapons: SplatoonFaker.mainWeapons(5).map((weaponSplId) => ({
|
||||
weaponSplId,
|
||||
isFavorite: 1,
|
||||
})),
|
||||
},
|
||||
},
|
||||
{
|
||||
...showcaseOptions(),
|
||||
patronTier: 2,
|
||||
card: {
|
||||
shortBio: faker.lorem.sentence(),
|
||||
bannerPresetImg: String(faker.helpers.arrayElement(stageIds)),
|
||||
unverifiedPeakXP: fakePeakXp(),
|
||||
},
|
||||
},
|
||||
);
|
||||
ids.push(maximalUser.id);
|
||||
|
||||
for (let i = ids.length; i < SHOWCASE_COUNT; i++) {
|
||||
const isArtist = artistIds.length < 12;
|
||||
const commissionsOpen = isArtist && faker.number.float(1) < 0.5;
|
||||
|
||||
const user = await UserFactory.create(
|
||||
{
|
||||
profile: {
|
||||
customName:
|
||||
faker.number.float(1) < 0.6
|
||||
? showcaseNames.customName()
|
||||
: undefined,
|
||||
customUrl: faker.number.float(1) < 0.2 ? `showcase-${i}` : undefined,
|
||||
country:
|
||||
faker.number.float(1) < 0.8 ? UserFactory.fakeCountry() : undefined,
|
||||
bio:
|
||||
faker.number.float(1) < 0.5 ? showcaseNames.postText() : undefined,
|
||||
inGameName:
|
||||
faker.number.float(1) < 0.4
|
||||
? showcaseNames.kanaInGameName()
|
||||
: SplatoonFaker.inGameName(),
|
||||
commissionsOpen: commissionsOpen ? 1 : undefined,
|
||||
commissionText: commissionsOpen ? faker.lorem.paragraph() : undefined,
|
||||
weapons: SplatoonFaker.mainWeapons(
|
||||
faker.helpers.arrayElement([1, 2, 3, 4]),
|
||||
).map((weaponSplId) => ({
|
||||
weaponSplId,
|
||||
isFavorite: faker.number.float(1) < 0.2 ? 1 : 0,
|
||||
})),
|
||||
},
|
||||
},
|
||||
{
|
||||
...showcaseOptions(),
|
||||
roles: isArtist ? ["ARTIST"] : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
ids.push(user.id);
|
||||
if (isArtist) {
|
||||
artistIds.push(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
return { ids, artistIds, favoriteBadgeUserIds };
|
||||
}
|
||||
|
||||
/** Widget profile of the one seeded supporter: both slots filled, and every widget
|
||||
* whose content other modules seed onto N-ZAP. */
|
||||
function nzapWidgets(): NonNullable<
|
||||
Parameters<typeof UserFactory.create>[1]
|
||||
>["widgets"] {
|
||||
return [
|
||||
{ id: "bio-md", settings: { bio: showcaseNames.maxLengthBio() } },
|
||||
{ id: "teams" },
|
||||
{ id: "organizations" },
|
||||
{ id: "patron-since" },
|
||||
{
|
||||
id: "sens",
|
||||
settings: { controller: "s2-pro-con", motionSens: 50, stickSens: 5 },
|
||||
},
|
||||
{ id: "timezone", settings: { timezone: "Europe/Stockholm" } },
|
||||
{ id: "social-links" },
|
||||
{ id: "weapon-pool" },
|
||||
{ id: "badges-owned" },
|
||||
{ id: "trophies-owned" },
|
||||
{ id: "builds" },
|
||||
{ id: "videos" },
|
||||
{ id: "art", settings: { source: "ALL" } },
|
||||
{ id: "x-rank-peaks", settings: { division: "both" } },
|
||||
{ id: "peak-sp" },
|
||||
{ id: "peak-xp" },
|
||||
{ id: "friends" },
|
||||
{ id: "highlighted-results" },
|
||||
];
|
||||
}
|
||||
|
||||
function showcaseOptions(): Parameters<typeof UserFactory.create>[1] {
|
||||
return {
|
||||
matchProfile: {
|
||||
mapModePreferences: fakePreferences(),
|
||||
vc: faker.helpers.arrayElement([
|
||||
"YES",
|
||||
"YES",
|
||||
"YES",
|
||||
"NO",
|
||||
"LISTEN_ONLY",
|
||||
]),
|
||||
languages: fakeLanguages(),
|
||||
weaponPool: SplatoonFaker.mainWeapons(
|
||||
faker.helpers.arrayElement([1, 2, 3, 4]),
|
||||
).map((id) => ({ id, isFavorite: faker.number.float(1) < 0.3 })),
|
||||
},
|
||||
div:
|
||||
faker.number.float(1) < 0.6
|
||||
? faker.helpers.arrayElement(LUTI_DIVS)
|
||||
: undefined,
|
||||
card: {
|
||||
shortBio: faker.number.float(1) < 0.6 ? faker.lorem.sentence() : null,
|
||||
bannerPresetImg: fakeBannerPresetImg(),
|
||||
unverifiedPeakXP: faker.number.float(1) < 0.4 ? fakePeakXp() : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fakeLanguages(): UnifiedLanguageCode[] {
|
||||
const languages: UnifiedLanguageCode[] =
|
||||
faker.number.float(1) > 0.1 ? ["en"] : [];
|
||||
for (const language of ["es", "fr", "de", "it", "ja"] as const) {
|
||||
if (faker.number.float(1) > 0.9) languages.push(language);
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
function fakePreferences(): UserMapModePreferences {
|
||||
const modes: UserMapModePreferences["modes"] = modesShort.flatMap((mode) => {
|
||||
if (faker.number.float(1) > 0.5 && mode !== "SZ") return [];
|
||||
|
||||
return {
|
||||
mode,
|
||||
preference:
|
||||
faker.number.float(1) > (mode === "SZ" ? 0.2 : 0.5)
|
||||
? ("PREFER" as const)
|
||||
: ("AVOID" as const),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
modes,
|
||||
pool: modesShort.flatMap((mode) => {
|
||||
const preference = modes.find((m) => m.mode === mode);
|
||||
if (preference?.preference === "AVOID") return [];
|
||||
|
||||
return {
|
||||
mode,
|
||||
stages: faker.helpers
|
||||
.shuffle(stageIds)
|
||||
.filter((stageId) => !BANNED_MAPS[mode].includes(stageId))
|
||||
.slice(0, AMOUNT_OF_MAPS_IN_POOL_PER_MODE),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Self-reported peak XP with exactly one division defined, as the column expects. */
|
||||
function fakePeakXp() {
|
||||
const points = faker.number.int({ min: 2000, max: 3500 });
|
||||
const isTentatek = faker.datatype.boolean();
|
||||
|
||||
return {
|
||||
overall: points,
|
||||
tentatek: isTentatek ? points : null,
|
||||
takoroka: isTentatek ? null : points,
|
||||
};
|
||||
}
|
||||
|
||||
/** Mix of the three banner sources: null, a stage banner, an explicit color. */
|
||||
function fakeBannerPresetImg() {
|
||||
const roll = faker.number.float(1);
|
||||
if (roll < 0.34) return null;
|
||||
if (roll < 0.67) return String(faker.helpers.arrayElement(stageIds));
|
||||
|
||||
return faker.helpers.arrayElement(PRESET_COLORS);
|
||||
}
|
||||
64
apps/web-react/app/db/seed/dev/vods.ts
Normal file
64
apps/web-react/app/db/seed/dev/vods.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { videoMatchTypes } from "~/features/vods/vods-constants";
|
||||
import {
|
||||
secondsToHoursMinutesSecondString,
|
||||
youtubeIdToYoutubeUrl,
|
||||
} from "~/features/vods/vods-utils";
|
||||
import { faker } from "../core/faker";
|
||||
import * as SplatoonFaker from "../core/SplatoonFaker";
|
||||
import * as VodFactory from "../factories/VodFactory";
|
||||
import type { SeededUsers } from "./users";
|
||||
|
||||
const VOD_COUNT = 6;
|
||||
const REAL_YOUTUBE_ID = "M4aV-BQWlVg";
|
||||
|
||||
export async function seedVods(users: SeededUsers) {
|
||||
await VodFactory.create({
|
||||
type: "TOURNAMENT",
|
||||
youtubeUrl: youtubeIdToYoutubeUrl(REAL_YOUTUBE_ID),
|
||||
date: { day: 2, month: 2, year: 2023 },
|
||||
submitterUserId: users.nzapId,
|
||||
title: "LUTI Division X Tournament - ABBF (THRONE) vs. Ascension",
|
||||
pov: { type: "USER", userId: users.nzapId },
|
||||
matches: fakeMatches(7),
|
||||
});
|
||||
|
||||
for (let i = 0; i < VOD_COUNT - 1; i++) {
|
||||
const type = videoMatchTypes[i % videoMatchTypes.length];
|
||||
const povUserId = faker.helpers.arrayElement(users.showcaseIds);
|
||||
|
||||
await VodFactory.create({
|
||||
type,
|
||||
submitterUserId:
|
||||
i % 4 === 0
|
||||
? users.nzapId
|
||||
: faker.helpers.arrayElement(users.showcaseIds),
|
||||
date: {
|
||||
day: faker.number.int({ min: 1, max: 28 }),
|
||||
month: faker.number.int({ min: 0, max: 11 }),
|
||||
year: faker.helpers.arrayElement([2023, 2024, 2025]),
|
||||
},
|
||||
pov:
|
||||
type === "CAST"
|
||||
? undefined
|
||||
: faker.number.float(1) < 0.8
|
||||
? { type: "USER", userId: povUserId }
|
||||
: { type: "NAME", name: faker.person.firstName() },
|
||||
matches: fakeMatches(faker.helpers.arrayElement([3, 4, 5, 6])),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function fakeMatches(count: number) {
|
||||
let secondsAt = 13;
|
||||
|
||||
return SplatoonFaker.mapList(count).map((map) => {
|
||||
const startsAt = secondsToHoursMinutesSecondString(secondsAt);
|
||||
secondsAt += faker.number.int({ min: 180, max: 500 });
|
||||
|
||||
return {
|
||||
...map,
|
||||
startsAt,
|
||||
weapons: SplatoonFaker.mainWeapons(faker.number.float(1) < 0.3 ? 8 : 1),
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user