mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
2700 lines
65 KiB
TypeScript
2700 lines
65 KiB
TypeScript
import { faker } from "@faker-js/faker";
|
|
import { add, sub } from "date-fns";
|
|
import * as R from "remeda";
|
|
import { db, sql } from "~/db/sql";
|
|
import { ADMIN_DISCORD_ID, ADMIN_ID } from "~/features/admin/admin-constants";
|
|
import type { SeedVariation } from "~/features/api-private/routes/seed";
|
|
import * as AssociationRepository from "~/features/associations/AssociationRepository.server";
|
|
import * as BuildRepository from "~/features/builds/BuildRepository.server";
|
|
import * as CalendarRepository from "~/features/calendar/CalendarRepository.server";
|
|
import { tags } from "~/features/calendar/calendar-constants";
|
|
import * as LFGRepository from "~/features/lfg/LFGRepository.server";
|
|
import { TIMEZONES } from "~/features/lfg/lfg-constants";
|
|
import { MapPool } from "~/features/map-list-generator/core/map-pool";
|
|
import * as NotificationRepository from "~/features/notifications/NotificationRepository.server";
|
|
import type { Notification } from "~/features/notifications/notifications-types";
|
|
import * as PlusSuggestionRepository from "~/features/plus-suggestions/PlusSuggestionRepository.server";
|
|
import {
|
|
lastCompletedVoting,
|
|
nextNonCompletedVoting,
|
|
rangeToMonthYear,
|
|
} from "~/features/plus-voting/core";
|
|
import * as PlusVotingRepository from "~/features/plus-voting/PlusVotingRepository.server";
|
|
import * as ScrimPostRepository from "~/features/scrims/ScrimPostRepository.server";
|
|
import { SendouQ } from "~/features/sendouq/core/SendouQ.server";
|
|
import * as SQGroupRepository from "~/features/sendouq/SQGroupRepository.server";
|
|
import { calculateMatchSkills } from "~/features/sendouq-match/core/skills.server";
|
|
import {
|
|
summarizeMaps,
|
|
summarizePlayerResults,
|
|
} from "~/features/sendouq-match/core/summarizer.server";
|
|
import { winnersArrayToWinner } from "~/features/sendouq-match/q-match-utils";
|
|
import { addMapResults } from "~/features/sendouq-match/queries/addMapResults.server";
|
|
import { addPlayerResults } from "~/features/sendouq-match/queries/addPlayerResults.server";
|
|
import { addReportedWeapons } from "~/features/sendouq-match/queries/addReportedWeapons.server";
|
|
import { addSkills } from "~/features/sendouq-match/queries/addSkills.server";
|
|
import { reportScore } from "~/features/sendouq-match/queries/reportScore.server";
|
|
import { setGroupAsInactive } from "~/features/sendouq-match/queries/setGroupAsInactive.server";
|
|
import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server";
|
|
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 { TOURNAMENT } from "~/features/tournament/tournament-constants";
|
|
import { clearAllTournamentDataCache } from "~/features/tournament-bracket/core/Tournament.server";
|
|
import * as TournamentOrganizationRepository from "~/features/tournament-organization/TournamentOrganizationRepository.server";
|
|
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
|
import * as VodRepository from "~/features/vods/VodRepository.server";
|
|
import {
|
|
secondsToHoursMinutesSecondString,
|
|
youtubeIdToYoutubeUrl,
|
|
} from "~/features/vods/vods-utils";
|
|
import { abilities } from "~/modules/in-game-lists/abilities";
|
|
import {
|
|
clothesGearIds,
|
|
headGearIds,
|
|
shoesGearIds,
|
|
} from "~/modules/in-game-lists/gear-ids";
|
|
import { modesShort, rankedModesShort } from "~/modules/in-game-lists/modes";
|
|
import { stagesObj as s, stageIds } from "~/modules/in-game-lists/stage-ids";
|
|
import type {
|
|
AbilityType,
|
|
MainWeaponId,
|
|
ModeShort,
|
|
StageId,
|
|
} from "~/modules/in-game-lists/types";
|
|
import { mainWeaponIds } from "~/modules/in-game-lists/weapon-ids";
|
|
import type { TournamentMapListMap } from "~/modules/tournament-map-list-generator/types";
|
|
import { nullFilledArray } from "~/utils/arrays";
|
|
import {
|
|
databaseTimestampNow,
|
|
databaseTimestampToDate,
|
|
dateToDatabaseTimestamp,
|
|
} from "~/utils/dates";
|
|
import { shortNanoid } from "~/utils/id";
|
|
import invariant from "~/utils/invariant";
|
|
import { mySlugify } from "~/utils/urls";
|
|
import {
|
|
getArtFilename,
|
|
SEED_ART_URLS,
|
|
SEED_TEAM_IMAGES,
|
|
SEED_TOURNAMENT_IMAGES,
|
|
} from "../../../scripts/seed-art-urls";
|
|
import type { QWeaponPool, Tables, UserMapModePreferences } from "../tables";
|
|
import {
|
|
ADMIN_TEST_AVATAR,
|
|
AMOUNT_OF_CALENDAR_EVENTS,
|
|
NZAP_TEST_AVATAR,
|
|
NZAP_TEST_DISCORD_ID,
|
|
NZAP_TEST_ID,
|
|
} from "./constants";
|
|
import placements from "./placements.json";
|
|
|
|
const SENDOUQ_DEFAULT_MAPS: Record<
|
|
ModeShort,
|
|
[StageId, StageId, StageId, StageId, StageId, StageId, StageId]
|
|
> = {
|
|
TW: [
|
|
s.EELTAIL_ALLEY,
|
|
s.HAGGLEFISH_MARKET,
|
|
s.UNDERTOW_SPILLWAY,
|
|
s.WAHOO_WORLD,
|
|
s.UM_AMI_RUINS,
|
|
s.HUMPBACK_PUMP_TRACK,
|
|
s.ROBO_ROM_EN,
|
|
],
|
|
SZ: [
|
|
s.HAGGLEFISH_MARKET,
|
|
s.MAHI_MAHI_RESORT,
|
|
s.INKBLOT_ART_ACADEMY,
|
|
s.MAKOMART,
|
|
s.HUMPBACK_PUMP_TRACK,
|
|
s.CRABLEG_CAPITAL,
|
|
s.ROBO_ROM_EN,
|
|
],
|
|
TC: [
|
|
s.ROBO_ROM_EN,
|
|
s.EELTAIL_ALLEY,
|
|
s.UNDERTOW_SPILLWAY,
|
|
s.MUSEUM_D_ALFONSINO,
|
|
s.MAKOMART,
|
|
s.MANTA_MARIA,
|
|
s.SHIPSHAPE_CARGO_CO,
|
|
],
|
|
RM: [
|
|
s.SCORCH_GORGE,
|
|
s.HAGGLEFISH_MARKET,
|
|
s.UNDERTOW_SPILLWAY,
|
|
s.MUSEUM_D_ALFONSINO,
|
|
s.FLOUNDER_HEIGHTS,
|
|
s.CRABLEG_CAPITAL,
|
|
s.MINCEMEAT_METALWORKS,
|
|
],
|
|
CB: [
|
|
s.SCORCH_GORGE,
|
|
s.INKBLOT_ART_ACADEMY,
|
|
s.BRINEWATER_SPRINGS,
|
|
s.MANTA_MARIA,
|
|
s.HUMPBACK_PUMP_TRACK,
|
|
s.UM_AMI_RUINS,
|
|
s.ROBO_ROM_EN,
|
|
],
|
|
};
|
|
|
|
const calendarEventWithToToolsRegOpen = () =>
|
|
calendarEventWithToTools("PICNIC", true);
|
|
|
|
const calendarEventWithToToolsSz = () => calendarEventWithToTools("ITZ");
|
|
const calendarEventWithToToolsTeamsSz = () =>
|
|
calendarEventWithToToolsTeams("ITZ");
|
|
|
|
const calendarEventWithToToolsPP = () => calendarEventWithToTools("PP");
|
|
const calendarEventWithToToolsPPRegOpen = () =>
|
|
calendarEventWithToTools("PP", true);
|
|
const calendarEventWithToToolsTeamsPP = () =>
|
|
calendarEventWithToToolsTeams("PP");
|
|
|
|
const calendarEventWithToToolsSOS = () => calendarEventWithToTools("SOS");
|
|
const calendarEventWithToToolsTeamsSOS = () =>
|
|
calendarEventWithToToolsTeams("SOS");
|
|
const calendarEventWithToToolsTeamsSOSSmall = () =>
|
|
calendarEventWithToToolsTeams("SOS", true);
|
|
|
|
const calendarEventWithToToolsDepths = () => calendarEventWithToTools("DEPTHS");
|
|
const calendarEventWithToToolsTeamsDepths = () =>
|
|
calendarEventWithToToolsTeams("DEPTHS");
|
|
|
|
const calendarEventWithToToolsLUTI = () => calendarEventWithToTools("LUTI");
|
|
const calendarEventWithToToolsTeamsLUTI = () =>
|
|
calendarEventWithToToolsTeams("LUTI");
|
|
|
|
const basicSeeds = (variation?: SeedVariation | null) => [
|
|
adminUser,
|
|
makeAdminPatron,
|
|
makeAdminVideoAdder,
|
|
makeAdminTournamentOrganizer,
|
|
nzapUser,
|
|
users,
|
|
fixAdminId,
|
|
adminUserWeaponPool,
|
|
userProfiles,
|
|
userMapModePreferences,
|
|
userQWeaponPool,
|
|
lastMonthsVoting,
|
|
syncPlusTiers,
|
|
lastMonthSuggestions,
|
|
thisMonthsSuggestions,
|
|
badgesToUsers,
|
|
badgeManagers,
|
|
patrons,
|
|
insertTeamAndTournamentImages,
|
|
organization,
|
|
calendarEvents,
|
|
calendarEventBadges,
|
|
calendarEventResults,
|
|
variation === "REG_OPEN"
|
|
? calendarEventWithToToolsRegOpen
|
|
: calendarEventWithToTools,
|
|
calendarEventWithToToolsTieBreakerMapPool,
|
|
variation === "NO_TOURNAMENT_TEAMS" || variation === "REG_OPEN"
|
|
? undefined
|
|
: calendarEventWithToToolsTeams,
|
|
calendarEventWithToToolsSz,
|
|
variation === "NO_TOURNAMENT_TEAMS"
|
|
? undefined
|
|
: calendarEventWithToToolsTeamsSz,
|
|
variation === "REG_OPEN"
|
|
? calendarEventWithToToolsPPRegOpen
|
|
: calendarEventWithToToolsPP,
|
|
variation === "NO_TOURNAMENT_TEAMS"
|
|
? undefined
|
|
: calendarEventWithToToolsTeamsPP,
|
|
calendarEventWithToToolsSOS,
|
|
variation === "SMALL_SOS"
|
|
? calendarEventWithToToolsTeamsSOSSmall
|
|
: calendarEventWithToToolsTeamsSOS,
|
|
calendarEventWithToToolsToSetMapPool,
|
|
calendarEventWithToToolsDepths,
|
|
calendarEventWithToToolsTeamsDepths,
|
|
calendarEventWithToToolsLUTI,
|
|
calendarEventWithToToolsTeamsLUTI,
|
|
tournamentSubs,
|
|
adminBuilds,
|
|
manySplattershotBuilds,
|
|
detailedTeam(variation),
|
|
otherTeams,
|
|
realVideo,
|
|
realVideoCast,
|
|
xRankPlacements,
|
|
arts,
|
|
commissionsOpen,
|
|
playedMatches,
|
|
variation === "NO_SQ_GROUPS" ? undefined : groups,
|
|
friendCodes,
|
|
lfgPosts,
|
|
variation === "NO_SCRIMS" ? undefined : scrimPosts,
|
|
variation === "NO_SCRIMS" ? undefined : scrimPostRequests,
|
|
associations,
|
|
notifications,
|
|
];
|
|
|
|
export async function seed(variation?: SeedVariation | null) {
|
|
wipeDB();
|
|
|
|
for (const seedFunc of basicSeeds(variation)) {
|
|
if (!seedFunc) continue;
|
|
|
|
faker.seed(5800);
|
|
|
|
await seedFunc();
|
|
}
|
|
|
|
clearAllTournamentDataCache();
|
|
}
|
|
|
|
function wipeDB() {
|
|
const tablesToDelete = [
|
|
"ScrimPost",
|
|
"TournamentOrganizationBannedUser",
|
|
"Association",
|
|
"LFGPost",
|
|
"Skill",
|
|
"ReportedWeapon",
|
|
"GroupMatchMap",
|
|
"GroupMatch",
|
|
"Group",
|
|
"ArtUserMetadata",
|
|
"Art",
|
|
"UnvalidatedUserSubmittedImage",
|
|
"AllTeamMember",
|
|
"AllTeam",
|
|
"Build",
|
|
"TournamentTeamMember",
|
|
"MapPoolMap",
|
|
"TournamentMatchGameResult",
|
|
"TournamentTeamCheckIn",
|
|
"TournamentTeam",
|
|
"TournamentStage",
|
|
"TournamentResult",
|
|
"Tournament",
|
|
"CalendarEventDate",
|
|
"CalendarEventResultPlayer",
|
|
"CalendarEventResultTeam",
|
|
"CalendarEventBadge",
|
|
"CalendarEvent",
|
|
"UserWeapon",
|
|
"PlusTier",
|
|
"UnvalidatedVideo",
|
|
"XRankPlacement",
|
|
"SplatoonPlayer",
|
|
"UserFriendCode",
|
|
"NotificationUser",
|
|
"Notification",
|
|
"BanLog",
|
|
"ModNote",
|
|
"User",
|
|
"PlusSuggestion",
|
|
"PlusVote",
|
|
"TournamentBadgeOwner",
|
|
"BadgeManager",
|
|
"TournamentOrganization",
|
|
];
|
|
|
|
for (const table of tablesToDelete) {
|
|
if (table === "Tournament") {
|
|
// foreign key constraint reasons
|
|
sql
|
|
.prepare("delete from Tournament where parentTournamentId is not null")
|
|
.run();
|
|
}
|
|
sql.prepare(`delete from "${table}"`).run();
|
|
}
|
|
}
|
|
|
|
async function adminUser() {
|
|
await UserRepository.upsert({
|
|
discordId: ADMIN_DISCORD_ID,
|
|
discordName: "Sendou",
|
|
twitch: "Sendou",
|
|
youtubeId: "UCWbJLXByvsfQvTcR4HLPs5Q",
|
|
discordAvatar: ADMIN_TEST_AVATAR,
|
|
discordUniqueName: "sendou",
|
|
});
|
|
}
|
|
|
|
function fixAdminId() {
|
|
sql.prepare(`delete from user where id = ${ADMIN_ID}`).run();
|
|
// make admin same ID as prod for easy switching
|
|
sql.prepare(`update "User" set "id" = ${ADMIN_ID} where id = 1`).run();
|
|
}
|
|
|
|
function makeAdminPatron() {
|
|
sql
|
|
.prepare(
|
|
`update "User" set "patronTier" = 2, "patronSince" = 1674663454 where id = 1`,
|
|
)
|
|
.run();
|
|
}
|
|
|
|
function makeAdminVideoAdder() {
|
|
sql.prepare(`update "User" set "isVideoAdder" = 1 where id = 1`).run();
|
|
}
|
|
|
|
function makeAdminTournamentOrganizer() {
|
|
sql
|
|
.prepare(`update "User" set "isTournamentOrganizer" = 1 where id = 1`)
|
|
.run();
|
|
}
|
|
|
|
function adminUserWeaponPool() {
|
|
for (const [i, weaponSplId] of [200, 1100, 2000, 4000].entries()) {
|
|
sql
|
|
.prepare(
|
|
`
|
|
insert into "UserWeapon" ("userId", "weaponSplId", "order")
|
|
values ($userId, $weaponSplId, $order)
|
|
`,
|
|
)
|
|
.run({ userId: ADMIN_ID, weaponSplId, order: i + 1 });
|
|
}
|
|
}
|
|
|
|
function nzapUser() {
|
|
return UserRepository.upsert({
|
|
discordId: NZAP_TEST_DISCORD_ID,
|
|
discordName: "N-ZAP",
|
|
twitch: null,
|
|
youtubeId: null,
|
|
discordAvatar: NZAP_TEST_AVATAR,
|
|
discordUniqueName: null,
|
|
});
|
|
}
|
|
|
|
async function users() {
|
|
const usedNames = new Set<string>();
|
|
for (let i = 0; i < 500; i++) {
|
|
const args = fakeUser(usedNames)();
|
|
|
|
await UserRepository.upsert(args);
|
|
}
|
|
}
|
|
|
|
async function userProfiles() {
|
|
for (const args of [
|
|
{
|
|
userId: ADMIN_ID,
|
|
country: "FI",
|
|
customUrl: "sendou",
|
|
motionSens: 50,
|
|
stickSens: 5,
|
|
inGameName: "Sendou#1234",
|
|
},
|
|
{
|
|
userId: 2,
|
|
country: "SE",
|
|
customUrl: "nzap",
|
|
motionSens: -40,
|
|
stickSens: 0,
|
|
inGameName: "N-ZAP#5678",
|
|
},
|
|
]) {
|
|
sql
|
|
.prepare(
|
|
`
|
|
UPDATE "User" SET
|
|
country = $country,
|
|
customUrl = $customUrl,
|
|
motionSens = $motionSens,
|
|
stickSens = $stickSens,
|
|
inGameName = $inGameName
|
|
WHERE id = $userId`,
|
|
)
|
|
.run(args);
|
|
}
|
|
|
|
for (let id = 2; id < 500; id++) {
|
|
if (id === ADMIN_ID || id === NZAP_TEST_ID) continue;
|
|
if (faker.number.float(1) < 0.25) continue; // 75% have bio
|
|
|
|
sql
|
|
.prepare(
|
|
`UPDATE "User" SET bio = $bio, country = $country WHERE id = $id`,
|
|
)
|
|
.run({
|
|
id,
|
|
bio: faker.lorem.paragraphs(
|
|
faker.helpers.arrayElement([1, 1, 1, 2, 3, 4]),
|
|
"\n\n",
|
|
),
|
|
country:
|
|
faker.number.float(1) > 0.5 ? faker.location.countryCode() : null,
|
|
});
|
|
}
|
|
|
|
for (let id = 2; id < 500; id++) {
|
|
if (id === ADMIN_ID || id === NZAP_TEST_ID) continue;
|
|
if (faker.number.float(1) < 0.15) continue; // 85% have weapons
|
|
|
|
const weapons = faker.helpers.shuffle(mainWeaponIds);
|
|
|
|
for (let j = 0; j < faker.helpers.arrayElement([1, 2, 3, 4, 5]); j++) {
|
|
sql
|
|
.prepare(
|
|
/* sql */ `insert into "UserWeapon" (
|
|
"userId",
|
|
"weaponSplId",
|
|
"order",
|
|
"isFavorite"
|
|
) values (
|
|
@userId,
|
|
@weaponSplId,
|
|
@order,
|
|
@isFavorite
|
|
)`,
|
|
)
|
|
.run({
|
|
userId: id,
|
|
weaponSplId: weapons.pop()!,
|
|
order: j + 1,
|
|
isFavorite: faker.number.float(1) > 0.8 ? 1 : 0,
|
|
});
|
|
}
|
|
}
|
|
|
|
for (let id = 1; id < 500; id++) {
|
|
const defaultLanguages = faker.number.float(1) > 0.1 ? ["en"] : [];
|
|
if (faker.number.float(1) > 0.9) defaultLanguages.push("es");
|
|
if (faker.number.float(1) > 0.9) defaultLanguages.push("fr");
|
|
if (faker.number.float(1) > 0.9) defaultLanguages.push("de");
|
|
if (faker.number.float(1) > 0.9) defaultLanguages.push("it");
|
|
if (faker.number.float(1) > 0.9) defaultLanguages.push("ja");
|
|
|
|
await QSettingsRepository.updateVoiceChat({
|
|
languages: defaultLanguages,
|
|
userId: id,
|
|
vc:
|
|
faker.number.float(1) > 0.2
|
|
? "YES"
|
|
: faker.helpers.arrayElement(["YES", "NO", "LISTEN_ONLY"]),
|
|
});
|
|
}
|
|
}
|
|
|
|
const randomPreferences = (): UserMapModePreferences => {
|
|
const modes: UserMapModePreferences["modes"] = modesShort.flatMap((mode) => {
|
|
if (faker.number.float(1) > 0.5 && mode !== "SZ") return [];
|
|
|
|
const criteria = mode === "SZ" ? 0.2 : 0.5;
|
|
|
|
return {
|
|
mode,
|
|
preference: faker.number.float(1) > criteria ? "PREFER" : "AVOID",
|
|
};
|
|
});
|
|
|
|
return {
|
|
modes,
|
|
pool: modesShort.flatMap((mode) => {
|
|
const mp = modes.find((m) => m.mode === mode);
|
|
if (mp?.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),
|
|
};
|
|
}),
|
|
};
|
|
};
|
|
|
|
async function userMapModePreferences() {
|
|
for (let id = 1; id < 500; id++) {
|
|
if (id !== ADMIN_ID && faker.number.float(1) < 0.2) continue; // 80% have maps && admin always
|
|
|
|
await db
|
|
.updateTable("User")
|
|
.where("User.id", "=", id)
|
|
.set({
|
|
mapModePreferences: JSON.stringify(randomPreferences()),
|
|
})
|
|
.execute();
|
|
}
|
|
}
|
|
|
|
async function userQWeaponPool() {
|
|
for (let id = 1; id < 500; id++) {
|
|
if (id === 2) continue; // no weapons for N-ZAP
|
|
if (faker.number.float(1) < 0.2) continue; // 80% have weapons
|
|
|
|
const weapons = faker.helpers
|
|
.shuffle(mainWeaponIds)
|
|
.slice(0, faker.helpers.arrayElement([1, 2, 3, 4]));
|
|
|
|
const weaponPool: Array<QWeaponPool> = weapons.map((weaponSplId) => ({
|
|
weaponSplId,
|
|
isFavorite: faker.number.float(1) > 0.7 ? 1 : 0,
|
|
}));
|
|
|
|
await db
|
|
.updateTable("User")
|
|
.set({ qWeaponPool: JSON.stringify(weaponPool) })
|
|
.where("User.id", "=", id)
|
|
.execute();
|
|
}
|
|
}
|
|
|
|
function fakeUser(usedNames: Set<string>) {
|
|
return () => ({
|
|
discordAvatar: null,
|
|
discordId: String(faker.string.numeric(17)),
|
|
discordName: uniqueDiscordName(usedNames),
|
|
twitch: null,
|
|
youtubeId: null,
|
|
discordUniqueName: null,
|
|
});
|
|
}
|
|
|
|
function uniqueDiscordName(usedNames: Set<string>) {
|
|
let result = faker.internet.username();
|
|
while (usedNames.has(result)) {
|
|
result = faker.internet.username();
|
|
}
|
|
usedNames.add(result);
|
|
|
|
return result;
|
|
}
|
|
|
|
const idToPlusTier = (id: number) => {
|
|
if (id < 30 || id === ADMIN_ID) return 1;
|
|
if (id < 80) return 2;
|
|
if (id <= 150) return 3;
|
|
|
|
// these ids failed the voting
|
|
if (id >= 200 && id <= 209) return 1;
|
|
if (id >= 210 && id <= 219) return 2;
|
|
if (id >= 220 && id <= 229) return 3;
|
|
|
|
throw new Error("Invalid id - no plus tier");
|
|
};
|
|
|
|
async function lastMonthsVoting() {
|
|
const votes = [];
|
|
|
|
const { month, year } = lastCompletedVoting(new Date());
|
|
|
|
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
|
|
|
for (let i = 1; i < 151; i++) {
|
|
if (i === NZAP_TEST_ID) continue; // omit N-ZAP user for testing;
|
|
|
|
const id = i === 1 ? ADMIN_ID : i;
|
|
|
|
votes.push({
|
|
authorId: ADMIN_ID,
|
|
month,
|
|
year,
|
|
score: 1,
|
|
tier: idToPlusTier(id),
|
|
validAfter: dateToDatabaseTimestamp(fiveMinutesAgo),
|
|
votedId: id,
|
|
});
|
|
}
|
|
|
|
for (let id = 200; id < 225; id++) {
|
|
votes.push({
|
|
authorId: ADMIN_ID,
|
|
month,
|
|
year,
|
|
score: -1,
|
|
tier: idToPlusTier(id),
|
|
validAfter: dateToDatabaseTimestamp(fiveMinutesAgo),
|
|
votedId: id,
|
|
});
|
|
}
|
|
|
|
await PlusVotingRepository.upsertMany(votes);
|
|
}
|
|
|
|
async function lastMonthSuggestions() {
|
|
const usersSuggested = [
|
|
3, 10, 14, 90, 120, 140, 200, 201, 203, 204, 205, 216, 217, 218, 219, 220,
|
|
];
|
|
const { month, year } = lastCompletedVoting(new Date());
|
|
|
|
for (const id of usersSuggested) {
|
|
await PlusSuggestionRepository.create({
|
|
authorId: ADMIN_ID,
|
|
month,
|
|
year,
|
|
suggestedId: id,
|
|
text: faker.lorem.lines(),
|
|
tier: idToPlusTier(id),
|
|
});
|
|
}
|
|
}
|
|
|
|
async function thisMonthsSuggestions() {
|
|
const usersInPlus = (await UserRepository.findAllPlusServerMembers()).filter(
|
|
(u) => u.userId !== ADMIN_ID,
|
|
);
|
|
const range = nextNonCompletedVoting(new Date());
|
|
invariant(range, "No next voting found");
|
|
const { month, year } = rangeToMonthYear(range);
|
|
|
|
for (let userId = 150; userId < 190; userId++) {
|
|
const amountOfSuggestions = faker.helpers.arrayElement([1, 1, 2, 3, 4]);
|
|
|
|
for (let i = 0; i < amountOfSuggestions; i++) {
|
|
const suggester = usersInPlus.shift();
|
|
invariant(suggester);
|
|
invariant(suggester.plusTier);
|
|
|
|
await PlusSuggestionRepository.create({
|
|
authorId: suggester.userId,
|
|
month,
|
|
year,
|
|
suggestedId: userId,
|
|
text: faker.lorem.lines(),
|
|
tier: suggester.plusTier,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function syncPlusTiers() {
|
|
sql
|
|
.prepare(
|
|
/* sql */ `
|
|
insert into "PlusTier" ("userId", "tier") select "userId", "tier" from "FreshPlusTier" where "tier" is not null;
|
|
`,
|
|
)
|
|
.run();
|
|
}
|
|
|
|
function getAvailableBadgeIds() {
|
|
return faker.helpers.shuffle(
|
|
(sql.prepare(`select "id" from "Badge"`).all() as any[]).map((b) => b.id),
|
|
);
|
|
}
|
|
|
|
function badgesToUsers() {
|
|
const availableBadgeIds = getAvailableBadgeIds();
|
|
|
|
let userIds = (
|
|
sql
|
|
.prepare(
|
|
`select "id" from "User" where id != ${NZAP_TEST_ID} and id != ${ADMIN_ID}`,
|
|
)
|
|
.all() as any[]
|
|
).map((u) => u.id) as number[];
|
|
|
|
const insertTournamentBadgeOwnerStm = sql.prepare(
|
|
`insert into "TournamentBadgeOwner" ("badgeId", "userId") values ($id, $userId)`,
|
|
);
|
|
|
|
for (const id of availableBadgeIds) {
|
|
userIds = faker.helpers.shuffle(userIds);
|
|
for (
|
|
let i = 0;
|
|
i <
|
|
faker.number.int({
|
|
min: 1,
|
|
max: 24,
|
|
});
|
|
i++
|
|
) {
|
|
const userToGetABadge = userIds.shift()!;
|
|
|
|
insertTournamentBadgeOwnerStm.run({ id, userId: userToGetABadge });
|
|
|
|
userIds.push(userToGetABadge);
|
|
}
|
|
}
|
|
|
|
for (const badgeId of nullFilledArray(20).map((_, i) => i + 1)) {
|
|
insertTournamentBadgeOwnerStm.run({ id: badgeId, userId: ADMIN_ID });
|
|
}
|
|
|
|
for (const badgeId of [5, 6, 7]) {
|
|
insertTournamentBadgeOwnerStm.run({ id: badgeId, userId: NZAP_TEST_ID });
|
|
}
|
|
}
|
|
|
|
function badgeManagers() {
|
|
// make N-ZAP user manager of several badges
|
|
for (let id = 1; id <= 10; id++) {
|
|
sql
|
|
.prepare(
|
|
`insert into "BadgeManager" ("badgeId", "userId") values ($id, $userId)`,
|
|
)
|
|
.run({ id, userId: NZAP_TEST_ID });
|
|
}
|
|
}
|
|
|
|
function patrons() {
|
|
const userIds = (
|
|
sql
|
|
.prepare(`select "id" from "User" order by random() limit 50`)
|
|
.all() as any[]
|
|
)
|
|
.map((u) => u.id)
|
|
.filter((id) => id !== NZAP_TEST_ID && id !== ADMIN_ID) as number[];
|
|
|
|
const givePatronStm = sql.prepare(
|
|
`update user set "patronTier" = $patronTier, "patronSince" = $patronSince where id = $id`,
|
|
);
|
|
for (const id of userIds) {
|
|
givePatronStm.run({
|
|
id,
|
|
patronSince: dateToDatabaseTimestamp(faker.date.past()),
|
|
patronTier: faker.helpers.arrayElement([1, 1, 2, 2, 2, 3, 3, 4]),
|
|
});
|
|
}
|
|
|
|
givePatronStm.run({
|
|
id: ADMIN_ID,
|
|
patronSince: dateToDatabaseTimestamp(faker.date.past()),
|
|
patronTier: 2,
|
|
});
|
|
}
|
|
|
|
function userIdsInRandomOrder(specialLast = false) {
|
|
const rows = (
|
|
sql.prepare(`select "id" from "User" order by random()`).all() as any[]
|
|
).map((u) => u.id) as number[];
|
|
|
|
if (!specialLast) return rows;
|
|
|
|
return [
|
|
...rows.filter((id) => id !== ADMIN_ID && id !== NZAP_TEST_ID),
|
|
ADMIN_ID,
|
|
NZAP_TEST_ID,
|
|
];
|
|
}
|
|
|
|
function userIdsInAscendingOrderById() {
|
|
const ids = (
|
|
sql.prepare(`select "id" from "User" order by id asc`).all() as any[]
|
|
).map((u) => u.id) as number[];
|
|
|
|
return [ADMIN_ID, ...ids.filter((id) => id !== ADMIN_ID)];
|
|
}
|
|
|
|
function calendarEvents() {
|
|
const userIds = userIdsInRandomOrder();
|
|
|
|
for (let id = 1; id <= AMOUNT_OF_CALENDAR_EVENTS; id++) {
|
|
const shuffledTags = faker.helpers.shuffle(Object.keys(tags));
|
|
|
|
sql
|
|
.prepare(
|
|
`
|
|
insert into "CalendarEvent" (
|
|
"id",
|
|
"name",
|
|
"description",
|
|
"discordInviteCode",
|
|
"bracketUrl",
|
|
"authorId",
|
|
"tags"
|
|
) values (
|
|
$id,
|
|
$name,
|
|
$description,
|
|
$discordInviteCode,
|
|
$bracketUrl,
|
|
$authorId,
|
|
$tags
|
|
)
|
|
`,
|
|
)
|
|
.run({
|
|
id,
|
|
name: `${R.capitalize(faker.word.adjective())} ${R.capitalize(
|
|
faker.word.noun(),
|
|
)}`,
|
|
description: faker.lorem.paragraph(),
|
|
discordInviteCode: faker.lorem.word(),
|
|
bracketUrl: faker.internet.url(),
|
|
authorId: id === 1 ? NZAP_TEST_ID : (userIds.pop() ?? null),
|
|
tags:
|
|
faker.number.float(1) > 0.2
|
|
? shuffledTags
|
|
.slice(
|
|
0,
|
|
faker.helpers.arrayElement([
|
|
1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 4, 5, 6,
|
|
]),
|
|
)
|
|
.join(",")
|
|
: null,
|
|
});
|
|
|
|
const twoDayEvent = faker.number.float(1) > 0.9;
|
|
const startTime =
|
|
id % 2 === 0
|
|
? faker.date.soon({ days: 42 })
|
|
: faker.date.recent({ days: 42 });
|
|
startTime.setMinutes(0, 0, 0);
|
|
|
|
sql
|
|
.prepare(
|
|
`
|
|
insert into "CalendarEventDate" (
|
|
"eventId",
|
|
"startTime"
|
|
) values (
|
|
$eventId,
|
|
$startTime
|
|
)
|
|
`,
|
|
)
|
|
.run({
|
|
eventId: id,
|
|
startTime: dateToDatabaseTimestamp(startTime),
|
|
});
|
|
|
|
if (twoDayEvent) {
|
|
startTime.setDate(startTime.getDate() + 1);
|
|
|
|
sql
|
|
.prepare(
|
|
`
|
|
insert into "CalendarEventDate" (
|
|
"eventId",
|
|
"startTime"
|
|
) values (
|
|
$eventId,
|
|
$startTime
|
|
)
|
|
`,
|
|
)
|
|
.run({
|
|
eventId: id,
|
|
startTime: dateToDatabaseTimestamp(startTime),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const addCalendarEventBadgeStm = sql.prepare(
|
|
/*sql */ `insert into "CalendarEventBadge"
|
|
("eventId", "badgeId")
|
|
values ($eventId, $badgeId)`,
|
|
);
|
|
|
|
function calendarEventBadges() {
|
|
for (let eventId = 1; eventId <= AMOUNT_OF_CALENDAR_EVENTS; eventId++) {
|
|
if (faker.number.float(1) > 0.25) continue;
|
|
|
|
const availableBadgeIds = getAvailableBadgeIds();
|
|
|
|
for (
|
|
let i = 0;
|
|
i < faker.helpers.arrayElement([1, 1, 1, 1, 2, 2, 3]);
|
|
i++
|
|
) {
|
|
addCalendarEventBadgeStm.run({
|
|
eventId,
|
|
badgeId: availableBadgeIds.pop(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
async function calendarEventResults() {
|
|
let userIds = userIdsInRandomOrder();
|
|
const eventIdsOfPast = new Set<number>(
|
|
(
|
|
sql
|
|
.prepare(
|
|
`select "CalendarEvent"."id"
|
|
from "CalendarEvent"
|
|
join "CalendarEventDate" on "CalendarEventDate"."eventId" = "CalendarEvent"."id"
|
|
where "CalendarEventDate"."startTime" < $startTime`,
|
|
)
|
|
.all({ startTime: dateToDatabaseTimestamp(new Date()) }) as any[]
|
|
).map((r) => r.id),
|
|
);
|
|
|
|
for (const eventId of eventIdsOfPast) {
|
|
// event id = 1 needs to be without results for e2e tests
|
|
if (faker.number.float(1) < 0.3 || eventId === 1) continue;
|
|
|
|
await CalendarRepository.upsertReportedScores({
|
|
eventId,
|
|
participantCount: faker.number.int({ min: 10, max: 250 }),
|
|
results: new Array(faker.helpers.arrayElement([1, 1, 2, 3, 3, 3, 8, 8]))
|
|
.fill(null)
|
|
.map((_, i) => ({
|
|
placement: i + 1,
|
|
teamName: R.capitalize(faker.word.noun()),
|
|
players: new Array(
|
|
faker.helpers.arrayElement([1, 2, 3, 4, 4, 4, 4, 4, 5, 6]),
|
|
)
|
|
.fill(null)
|
|
.map(() => {
|
|
const withStringName = faker.number.float(1) < 0.2;
|
|
|
|
return {
|
|
name: withStringName ? faker.person.firstName() : null,
|
|
userId: withStringName ? null : userIds.pop()!,
|
|
};
|
|
}),
|
|
})),
|
|
});
|
|
|
|
userIds = userIdsInRandomOrder();
|
|
}
|
|
}
|
|
|
|
const TO_TOOLS_CALENDAR_EVENT_ID = 201;
|
|
function calendarEventWithToTools(
|
|
event: "PICNIC" | "ITZ" | "PP" | "SOS" | "DEPTHS" | "LUTI" = "PICNIC",
|
|
registrationOpen = false,
|
|
) {
|
|
const tournamentId = {
|
|
PICNIC: 1,
|
|
ITZ: 2,
|
|
PP: 3,
|
|
SOS: 4,
|
|
DEPTHS: 5,
|
|
LUTI: 6,
|
|
}[event];
|
|
const eventId = {
|
|
PICNIC: TO_TOOLS_CALENDAR_EVENT_ID + 0,
|
|
ITZ: TO_TOOLS_CALENDAR_EVENT_ID + 1,
|
|
PP: TO_TOOLS_CALENDAR_EVENT_ID + 2,
|
|
SOS: TO_TOOLS_CALENDAR_EVENT_ID + 3,
|
|
DEPTHS: TO_TOOLS_CALENDAR_EVENT_ID + 4,
|
|
LUTI: TO_TOOLS_CALENDAR_EVENT_ID + 5,
|
|
}[event];
|
|
const name = {
|
|
PICNIC: "PICNIC #2",
|
|
ITZ: "In The Zone 22",
|
|
PP: "Paddling Pool 253",
|
|
SOS: "Swim or Sink 101",
|
|
DEPTHS: "The Depths 5",
|
|
LUTI: "Leagues Under The Ink Season 15",
|
|
}[event];
|
|
const badges = {
|
|
PICNIC: [1, 2],
|
|
ITZ: [3, 4],
|
|
PP: [5, 6],
|
|
SOS: [7, 8],
|
|
DEPTHS: [9, 10],
|
|
LUTI: [],
|
|
}[event];
|
|
|
|
const settings: Tables["Tournament"]["settings"] =
|
|
event === "DEPTHS"
|
|
? {
|
|
bracketProgression: [
|
|
{
|
|
type: "swiss",
|
|
name: "Swiss",
|
|
requiresCheckIn: false,
|
|
settings: {
|
|
groupCount: 2,
|
|
roundCount: 4,
|
|
},
|
|
},
|
|
{
|
|
type: "single_elimination",
|
|
name: "Top Cut",
|
|
requiresCheckIn: false,
|
|
settings: {
|
|
thirdPlaceMatch: false,
|
|
},
|
|
sources: [
|
|
{
|
|
bracketIdx: 0,
|
|
placements: [1, 2, 3, 4],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
enableNoScreenToggle: true,
|
|
isRanked: false,
|
|
}
|
|
: event === "SOS"
|
|
? {
|
|
bracketProgression: [
|
|
{
|
|
type: "round_robin",
|
|
name: "Groups stage",
|
|
requiresCheckIn: false,
|
|
settings: {},
|
|
},
|
|
{
|
|
type: "single_elimination",
|
|
name: "Great White",
|
|
requiresCheckIn: false,
|
|
settings: {},
|
|
sources: [{ bracketIdx: 0, placements: [1] }],
|
|
},
|
|
{
|
|
type: "single_elimination",
|
|
name: "Hammerhead",
|
|
requiresCheckIn: false,
|
|
settings: {},
|
|
sources: [{ bracketIdx: 0, placements: [2] }],
|
|
},
|
|
{
|
|
type: "single_elimination",
|
|
name: "Mako",
|
|
requiresCheckIn: false,
|
|
settings: {},
|
|
sources: [{ bracketIdx: 0, placements: [3] }],
|
|
},
|
|
{
|
|
type: "single_elimination",
|
|
name: "Lantern",
|
|
requiresCheckIn: false,
|
|
settings: {},
|
|
sources: [{ bracketIdx: 0, placements: [4] }],
|
|
},
|
|
],
|
|
enableNoScreenToggle: true,
|
|
}
|
|
: event === "PP"
|
|
? {
|
|
bracketProgression: [
|
|
{
|
|
type: "round_robin",
|
|
name: "Groups stage",
|
|
requiresCheckIn: false,
|
|
settings: {},
|
|
},
|
|
{
|
|
type: "single_elimination",
|
|
name: "Final stage",
|
|
requiresCheckIn: false,
|
|
settings: {},
|
|
sources: [{ bracketIdx: 0, placements: [1, 2] }],
|
|
},
|
|
{
|
|
type: "single_elimination",
|
|
name: "Underground bracket",
|
|
requiresCheckIn: true,
|
|
settings: {},
|
|
sources: [{ bracketIdx: 0, placements: [3, 4] }],
|
|
},
|
|
],
|
|
}
|
|
: event === "ITZ"
|
|
? {
|
|
bracketProgression: [
|
|
{
|
|
type: "double_elimination",
|
|
name: "Main bracket",
|
|
requiresCheckIn: false,
|
|
settings: {},
|
|
},
|
|
{
|
|
type: "single_elimination",
|
|
name: "Underground bracket",
|
|
requiresCheckIn: false,
|
|
settings: {},
|
|
sources: [{ bracketIdx: 0, placements: [-1, -2] }],
|
|
},
|
|
],
|
|
}
|
|
: event === "LUTI"
|
|
? {
|
|
bracketProgression: [
|
|
{
|
|
type: "round_robin",
|
|
name: "Groups stage",
|
|
requiresCheckIn: false,
|
|
settings: {},
|
|
},
|
|
{
|
|
type: "single_elimination",
|
|
name: "Play-offs",
|
|
requiresCheckIn: false,
|
|
settings: {},
|
|
sources: [{ bracketIdx: 0, placements: [1, 2] }],
|
|
},
|
|
],
|
|
}
|
|
: {
|
|
bracketProgression: [
|
|
{
|
|
type: "double_elimination",
|
|
name: "Main bracket",
|
|
requiresCheckIn: false,
|
|
settings: {},
|
|
},
|
|
],
|
|
};
|
|
|
|
sql
|
|
.prepare(
|
|
`
|
|
insert into "Tournament" (
|
|
"id",
|
|
"mapPickingStyle",
|
|
"settings"
|
|
) values (
|
|
$id,
|
|
$mapPickingStyle,
|
|
$settings
|
|
) returning *
|
|
`,
|
|
)
|
|
.run({
|
|
id: tournamentId,
|
|
settings: JSON.stringify(settings),
|
|
mapPickingStyle:
|
|
event === "SOS" || event === "LUTI"
|
|
? "TO"
|
|
: event === "ITZ"
|
|
? "AUTO_SZ"
|
|
: "AUTO_ALL",
|
|
});
|
|
|
|
sql
|
|
.prepare(
|
|
`
|
|
insert into "CalendarEvent" (
|
|
"id",
|
|
"name",
|
|
"description",
|
|
"discordInviteCode",
|
|
"bracketUrl",
|
|
"authorId",
|
|
"tournamentId",
|
|
"organizationId",
|
|
"avatarImgId"
|
|
) values (
|
|
$id,
|
|
$name,
|
|
$description,
|
|
$discordInviteCode,
|
|
$bracketUrl,
|
|
$authorId,
|
|
$tournamentId,
|
|
$organizationId,
|
|
$avatarImgId
|
|
)
|
|
`,
|
|
)
|
|
.run({
|
|
id: eventId,
|
|
name,
|
|
description: faker.lorem.paragraph(),
|
|
discordInviteCode: faker.lorem.word(),
|
|
bracketUrl: faker.internet.url(),
|
|
authorId: ADMIN_ID,
|
|
tournamentId,
|
|
organizationId: event === "PICNIC" ? 1 : null,
|
|
avatarImgId: getTournamentImageId(tournamentId),
|
|
});
|
|
|
|
const halfAnHourFromNow = new Date(Date.now() + 1000 * 60 * 30);
|
|
|
|
sql
|
|
.prepare(
|
|
`
|
|
insert into "CalendarEventDate" (
|
|
"eventId",
|
|
"startTime"
|
|
) values (
|
|
$eventId,
|
|
$startTime
|
|
)
|
|
`,
|
|
)
|
|
.run({
|
|
eventId,
|
|
startTime: dateToDatabaseTimestamp(
|
|
registrationOpen
|
|
? halfAnHourFromNow
|
|
: new Date(Date.now() - 1000 * 60 * 60),
|
|
),
|
|
});
|
|
|
|
for (const badgeId of badges) {
|
|
addCalendarEventBadgeStm.run({
|
|
eventId,
|
|
badgeId,
|
|
});
|
|
}
|
|
}
|
|
|
|
const tiebreakerPicks = new MapPool([
|
|
{ mode: "SZ", stageId: 1 },
|
|
{ mode: "TC", stageId: 2 },
|
|
{ mode: "RM", stageId: 3 },
|
|
{ mode: "CB", stageId: 4 },
|
|
]);
|
|
function calendarEventWithToToolsTieBreakerMapPool() {
|
|
for (const tieBreakerCalendarEventId of [
|
|
TO_TOOLS_CALENDAR_EVENT_ID, // PICNIC
|
|
TO_TOOLS_CALENDAR_EVENT_ID + 2, // Paddling Pool
|
|
TO_TOOLS_CALENDAR_EVENT_ID + 4, // The Depths
|
|
]) {
|
|
for (const { mode, stageId } of tiebreakerPicks.stageModePairs) {
|
|
sql
|
|
.prepare(
|
|
`
|
|
insert into "MapPoolMap" (
|
|
"tieBreakerCalendarEventId",
|
|
"stageId",
|
|
"mode"
|
|
) values (
|
|
$tieBreakerCalendarEventId,
|
|
$stageId,
|
|
$mode
|
|
)
|
|
`,
|
|
)
|
|
.run({
|
|
tieBreakerCalendarEventId,
|
|
stageId,
|
|
mode,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function calendarEventWithToToolsToSetMapPool() {
|
|
const stages = [
|
|
...SENDOUQ_DEFAULT_MAPS.SZ.map((stageId) => ({ mode: "SZ", stageId })),
|
|
...SENDOUQ_DEFAULT_MAPS.TC.map((stageId) => ({ mode: "TC", stageId })),
|
|
...SENDOUQ_DEFAULT_MAPS.RM.map((stageId) => ({ mode: "RM", stageId })),
|
|
...SENDOUQ_DEFAULT_MAPS.CB.map((stageId) => ({ mode: "CB", stageId })),
|
|
];
|
|
|
|
for (const { mode, stageId } of stages) {
|
|
sql
|
|
.prepare(
|
|
`
|
|
insert into "MapPoolMap" (
|
|
"calendarEventId",
|
|
"stageId",
|
|
"mode"
|
|
) values (
|
|
$calendarEventId,
|
|
$stageId,
|
|
$mode
|
|
)
|
|
`,
|
|
)
|
|
.run({
|
|
calendarEventId: TO_TOOLS_CALENDAR_EVENT_ID + 3,
|
|
stageId,
|
|
mode,
|
|
});
|
|
}
|
|
}
|
|
|
|
const validTournamentTeamName = () => {
|
|
while (true) {
|
|
const name = faker.music.songName();
|
|
if (name.length <= TOURNAMENT.TEAM_NAME_MAX_LENGTH) return name;
|
|
}
|
|
};
|
|
|
|
const availableStages: StageId[] = [1, 2, 3, 4, 6, 7, 8, 10, 11];
|
|
const availablePairs = rankedModesShort
|
|
.flatMap((mode) =>
|
|
availableStages.map((stageId) => ({ mode, stageId: stageId })),
|
|
)
|
|
.filter((pair) => !tiebreakerPicks.has(pair));
|
|
function calendarEventWithToToolsTeams(
|
|
event: "PICNIC" | "ITZ" | "PP" | "SOS" | "DEPTHS" | "LUTI" = "PICNIC",
|
|
isSmall = false,
|
|
) {
|
|
const userIds = userIdsInAscendingOrderById();
|
|
const names = Array.from(
|
|
new Set(new Array(100).fill(null).map(() => validTournamentTeamName())),
|
|
).concat("Chimera");
|
|
|
|
const tournamentId = {
|
|
PICNIC: 1,
|
|
ITZ: 2,
|
|
PP: 3,
|
|
SOS: 4,
|
|
DEPTHS: 5,
|
|
LUTI: 6,
|
|
}[event];
|
|
|
|
const teamIdAddition = {
|
|
PICNIC: 0,
|
|
ITZ: 100,
|
|
PP: 200,
|
|
SOS: 300,
|
|
DEPTHS: 400,
|
|
LUTI: 500,
|
|
}[event];
|
|
|
|
for (let id = 1; id <= (isSmall ? 4 : 16); id++) {
|
|
const teamId = id + teamIdAddition;
|
|
|
|
const name = names.pop();
|
|
invariant(name, "tournament team name is falsy");
|
|
|
|
sql
|
|
.prepare(
|
|
`
|
|
insert into "TournamentTeam" (
|
|
"id",
|
|
"name",
|
|
"createdAt",
|
|
"tournamentId",
|
|
"inviteCode"
|
|
) values (
|
|
$id,
|
|
$name,
|
|
$createdAt,
|
|
$tournamentId,
|
|
$inviteCode
|
|
)
|
|
`,
|
|
)
|
|
.run({
|
|
id: teamId,
|
|
name,
|
|
createdAt: dateToDatabaseTimestamp(new Date()),
|
|
tournamentId,
|
|
inviteCode: shortNanoid(),
|
|
});
|
|
|
|
// in PICNIC & PP Chimera is not checked in + in LUTI no check-ins at all
|
|
if (teamId !== 1 && teamId !== 201 && event !== "LUTI") {
|
|
sql
|
|
.prepare(
|
|
`
|
|
insert into "TournamentTeamCheckIn" (
|
|
"tournamentTeamId",
|
|
"checkedInAt"
|
|
) values (
|
|
$tournamentTeamId,
|
|
$checkedInAt
|
|
)
|
|
`,
|
|
)
|
|
.run({
|
|
tournamentTeamId: teamId,
|
|
checkedInAt: dateToDatabaseTimestamp(new Date()),
|
|
});
|
|
}
|
|
|
|
for (let i = 0; i < (id < 10 ? 4 : 5); i++) {
|
|
let userId = userIds.shift()!;
|
|
// ensure N-ZAP is in different team than Sendou for ITZ
|
|
if (userId === NZAP_TEST_ID && teamId === 101) {
|
|
userId = userIds.shift()!;
|
|
userIds.unshift(NZAP_TEST_ID);
|
|
}
|
|
|
|
// prevent everyone showing as subs
|
|
const yesterday = new Date();
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
|
|
sql
|
|
.prepare(
|
|
`
|
|
insert into "TournamentTeamMember" (
|
|
"tournamentTeamId",
|
|
"userId",
|
|
"isOwner",
|
|
"createdAt"
|
|
) values (
|
|
$tournamentTeamId,
|
|
$userId,
|
|
$isOwner,
|
|
$createdAt
|
|
)
|
|
`,
|
|
)
|
|
.run({
|
|
tournamentTeamId: id + teamIdAddition,
|
|
userId,
|
|
isOwner: i === 0 ? 1 : 0,
|
|
createdAt: dateToDatabaseTimestamp(yesterday),
|
|
});
|
|
}
|
|
|
|
if (
|
|
event !== "SOS" &&
|
|
event !== "LUTI" &&
|
|
(faker.number.float(1) < 0.8 || id === 1)
|
|
) {
|
|
const shuffledPairs = faker.helpers.shuffle(availablePairs.slice());
|
|
|
|
let SZ = 0;
|
|
let TC = 0;
|
|
let RM = 0;
|
|
let CB = 0;
|
|
const stageUsedCounts: Partial<Record<StageId, number>> = {};
|
|
|
|
for (const pair of shuffledPairs) {
|
|
if (event === "ITZ" && pair.mode !== "SZ") continue;
|
|
if (BANNED_MAPS[pair.mode].includes(pair.stageId)) {
|
|
continue;
|
|
}
|
|
|
|
if (pair.mode === "SZ" && SZ >= (event === "ITZ" ? 6 : 2)) continue;
|
|
if (pair.mode === "TC" && TC >= 2) continue;
|
|
if (pair.mode === "RM" && RM >= 2) continue;
|
|
if (pair.mode === "CB" && CB >= 2) continue;
|
|
|
|
if (stageUsedCounts[pair.stageId] === (event === "ITZ" ? 1 : 2))
|
|
continue;
|
|
|
|
stageUsedCounts[pair.stageId] =
|
|
(stageUsedCounts[pair.stageId] ?? 0) + 1;
|
|
|
|
sql
|
|
.prepare(
|
|
`
|
|
insert into "MapPoolMap" (
|
|
"tournamentTeamId",
|
|
"stageId",
|
|
"mode"
|
|
) values (
|
|
$tournamentTeamId,
|
|
$stageId,
|
|
$mode
|
|
)
|
|
`,
|
|
)
|
|
.run({
|
|
tournamentTeamId: id + teamIdAddition,
|
|
stageId: pair.stageId,
|
|
mode: pair.mode,
|
|
});
|
|
|
|
if (pair.mode === "SZ") SZ++;
|
|
if (pair.mode === "TC") TC++;
|
|
if (pair.mode === "RM") RM++;
|
|
if (pair.mode === "CB") CB++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function tournamentSubs() {
|
|
for (let id = 100; id < 120; id++) {
|
|
const includedWeaponIds: MainWeaponId[] = [];
|
|
|
|
sql
|
|
.prepare(
|
|
/* sql */ `
|
|
insert into "TournamentSub" (
|
|
"userId",
|
|
"tournamentId",
|
|
"canVc",
|
|
"bestWeapons",
|
|
"okWeapons",
|
|
"message",
|
|
"visibility"
|
|
) values (
|
|
@userId,
|
|
@tournamentId,
|
|
@canVc,
|
|
@bestWeapons,
|
|
@okWeapons,
|
|
@message,
|
|
@visibility
|
|
)
|
|
`,
|
|
)
|
|
.run({
|
|
userId: id,
|
|
tournamentId: 1,
|
|
canVc: Number(faker.number.float(1) > 0.5),
|
|
bestWeapons: nullFilledArray(
|
|
faker.helpers.arrayElement([1, 1, 1, 2, 2, 3, 4, 5]),
|
|
)
|
|
// biome-ignore lint/suspicious/useIterableCallbackReturn: Biome 2.3.1 upgrade
|
|
.map(() => {
|
|
while (true) {
|
|
const weaponId = R.sample(mainWeaponIds, 1)[0]!;
|
|
if (!includedWeaponIds.includes(weaponId)) {
|
|
includedWeaponIds.push(weaponId);
|
|
return weaponId;
|
|
}
|
|
}
|
|
})
|
|
.join(","),
|
|
okWeapons:
|
|
faker.number.float(1) > 0.5
|
|
? null
|
|
: nullFilledArray(
|
|
faker.helpers.arrayElement([1, 1, 1, 2, 2, 3, 4, 5]),
|
|
)
|
|
// biome-ignore lint/suspicious/useIterableCallbackReturn: Biome 2.3.1 upgrade
|
|
.map(() => {
|
|
while (true) {
|
|
const weaponId = R.sample(mainWeaponIds, 1)[0]!;
|
|
if (!includedWeaponIds.includes(weaponId)) {
|
|
includedWeaponIds.push(weaponId);
|
|
return weaponId;
|
|
}
|
|
}
|
|
})
|
|
.join(","),
|
|
message: faker.number.float(1) > 0.5 ? null : faker.lorem.paragraph(),
|
|
visibility: id < 105 ? "+1" : id < 110 ? "+2" : id < 115 ? "+2" : "ALL",
|
|
});
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
const randomAbility = (legalTypes: AbilityType[]) => {
|
|
const randomOrderAbilities = faker.helpers.shuffle([...abilities]);
|
|
|
|
return randomOrderAbilities.find((a) => legalTypes.includes(a.type))!.name;
|
|
};
|
|
|
|
const adminWeaponPool = mainWeaponIds.filter(() => faker.number.float(1) > 0.8);
|
|
async function adminBuilds() {
|
|
for (let i = 0; i < 50; i++) {
|
|
const randomOrderHeadGear = faker.helpers.shuffle(headGearIds.slice());
|
|
const randomOrderClothesGear = faker.helpers.shuffle(
|
|
clothesGearIds.slice(),
|
|
);
|
|
const randomOrderShoesGear = faker.helpers.shuffle(shoesGearIds.slice());
|
|
// filter out sshot to prevent test flaking
|
|
const randomOrderWeaponIds = faker.helpers.shuffle(
|
|
adminWeaponPool.filter((id) => id !== 40).slice(),
|
|
);
|
|
|
|
await BuildRepository.create({
|
|
title: `${R.capitalize(faker.word.adjective())} ${R.capitalize(
|
|
faker.word.noun(),
|
|
)}`,
|
|
ownerId: ADMIN_ID,
|
|
private: 0,
|
|
description:
|
|
faker.number.float(1) < 0.75 ? faker.lorem.paragraph() : null,
|
|
headGearSplId: randomOrderHeadGear[0],
|
|
clothesGearSplId: randomOrderClothesGear[0],
|
|
shoesGearSplId: randomOrderShoesGear[0],
|
|
weaponSplIds: new Array(
|
|
faker.helpers.arrayElement([1, 1, 1, 2, 2, 3, 4, 5]),
|
|
)
|
|
.fill(null)
|
|
.map(() => randomOrderWeaponIds.pop()!),
|
|
modes:
|
|
faker.number.float(1) < 0.75
|
|
? modesShort.filter(() => faker.number.float(1) < 0.5)
|
|
: null,
|
|
abilities: [
|
|
[
|
|
randomAbility(["HEAD_MAIN_ONLY", "STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
],
|
|
[
|
|
randomAbility(["CLOTHES_MAIN_ONLY", "STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
],
|
|
[
|
|
randomAbility(["SHOES_MAIN_ONLY", "STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
],
|
|
],
|
|
});
|
|
}
|
|
}
|
|
|
|
async function manySplattershotBuilds() {
|
|
// ensure 500 has at least one splattershot build for x placement test
|
|
const users = [
|
|
...userIdsInRandomOrder().filter(
|
|
(id) => id !== 500 && id !== ADMIN_ID && id !== NZAP_TEST_ID,
|
|
),
|
|
500,
|
|
];
|
|
|
|
for (let i = 0; i < 499; i++) {
|
|
const SPLATTERSHOT_ID = 40;
|
|
|
|
const randomOrderHeadGear = faker.helpers.shuffle(headGearIds.slice());
|
|
const randomOrderClothesGear = faker.helpers.shuffle(
|
|
clothesGearIds.slice(),
|
|
);
|
|
const randomOrderShoesGear = faker.helpers.shuffle(shoesGearIds.slice());
|
|
const randomOrderWeaponIds = faker.helpers
|
|
.shuffle(mainWeaponIds.slice())
|
|
.filter((id) => id !== SPLATTERSHOT_ID);
|
|
|
|
const ownerId = users.pop()!;
|
|
|
|
await BuildRepository.create({
|
|
private: 0,
|
|
title: `${R.capitalize(faker.word.adjective())} ${R.capitalize(
|
|
faker.word.noun(),
|
|
)}`,
|
|
ownerId,
|
|
description:
|
|
faker.number.float(1) < 0.75 ? faker.lorem.paragraph() : null,
|
|
headGearSplId: randomOrderHeadGear[0],
|
|
clothesGearSplId: randomOrderClothesGear[0],
|
|
shoesGearSplId: randomOrderShoesGear[0],
|
|
weaponSplIds: new Array(
|
|
faker.helpers.arrayElement([1, 1, 1, 2, 2, 3, 4, 5]),
|
|
)
|
|
.fill(null)
|
|
.map((_, i) =>
|
|
i === 0 ? SPLATTERSHOT_ID : randomOrderWeaponIds.pop()!,
|
|
),
|
|
modes:
|
|
faker.number.float(1) < 0.75
|
|
? modesShort.filter(() => faker.number.float(1) < 0.5)
|
|
: null,
|
|
abilities: [
|
|
[
|
|
randomAbility(["HEAD_MAIN_ONLY", "STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
],
|
|
[
|
|
randomAbility(["CLOTHES_MAIN_ONLY", "STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
],
|
|
[
|
|
randomAbility(["SHOES_MAIN_ONLY", "STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
randomAbility(["STACKABLE"]),
|
|
],
|
|
],
|
|
});
|
|
}
|
|
}
|
|
|
|
const detailedTeam = (seedVariation?: SeedVariation | null) => () => {
|
|
sql
|
|
.prepare(
|
|
/* sql */ `
|
|
insert into "AllTeam" ("name", "customUrl", "inviteCode", "bio", "avatarImgId")
|
|
values (
|
|
'Alliance Rogue',
|
|
'alliance-rogue',
|
|
'${shortNanoid()}',
|
|
'${faker.lorem.paragraph()}',
|
|
${getTeamImageId(1)}
|
|
)
|
|
`,
|
|
)
|
|
.run();
|
|
|
|
const userIds = userIdsInRandomOrder(true).filter(
|
|
(id) => id !== NZAP_TEST_ID,
|
|
);
|
|
if (seedVariation === "NZAP_IN_TEAM") {
|
|
userIds.unshift(NZAP_TEST_ID);
|
|
}
|
|
for (let i = 0; i < 5; i++) {
|
|
const userId = i === 0 ? ADMIN_ID : userIds.shift()!;
|
|
|
|
sql
|
|
.prepare(
|
|
/*sql*/ `
|
|
insert into "AllTeamMember" ("teamId", "userId", "role", "isOwner", "leftAt")
|
|
values (
|
|
1,
|
|
${userId},
|
|
${i === 0 ? "'CAPTAIN'" : "'FRONTLINE'"},
|
|
${i === 0 ? 1 : 0},
|
|
${i < 4 ? "null" : "1672587342"}
|
|
)
|
|
`,
|
|
)
|
|
.run();
|
|
}
|
|
};
|
|
|
|
function otherTeams() {
|
|
const usersInTeam = (
|
|
sql
|
|
.prepare(
|
|
/*sql */ `select
|
|
"userId"
|
|
from "AllTeamMember"
|
|
`,
|
|
)
|
|
.all() as any[]
|
|
).map((row) => row.userId);
|
|
|
|
const userIds = userIdsInRandomOrder().filter(
|
|
(u) => !usersInTeam.includes(u) && u !== NZAP_TEST_ID,
|
|
);
|
|
|
|
for (let i = 3; i < 50; i++) {
|
|
const teamName =
|
|
i === 3
|
|
? "Team Olive"
|
|
: `${R.capitalize(faker.word.adjective())} ${R.capitalize(
|
|
faker.word.noun(),
|
|
)}`;
|
|
const teamCustomUrl = mySlugify(teamName);
|
|
|
|
sql
|
|
.prepare(
|
|
/* sql */ `
|
|
insert into "AllTeam" ("id", "name", "customUrl", "inviteCode", "bio")
|
|
values (
|
|
@id,
|
|
@name,
|
|
@customUrl,
|
|
@inviteCode,
|
|
@bio
|
|
)
|
|
`,
|
|
)
|
|
.run({
|
|
id: i,
|
|
name: teamName,
|
|
customUrl: teamCustomUrl,
|
|
inviteCode: shortNanoid(),
|
|
bio: faker.lorem.paragraph(),
|
|
});
|
|
|
|
const numMembers = faker.helpers.arrayElement([
|
|
1, 2, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 7, 7, 8,
|
|
]);
|
|
for (let j = 0; j < numMembers; j++) {
|
|
const userId = userIds.shift()!;
|
|
|
|
sql
|
|
.prepare(
|
|
/*sql*/ `
|
|
insert into "AllTeamMember" ("teamId", "userId", "role", "isOwner")
|
|
values (
|
|
${i},
|
|
${userId},
|
|
${j === 0 ? "'CAPTAIN'" : "'FRONTLINE'"},
|
|
${j === 0 ? 1 : 0}
|
|
)
|
|
`,
|
|
)
|
|
.run();
|
|
}
|
|
}
|
|
}
|
|
|
|
async function realVideo() {
|
|
for (let i = 0; i < 5; i++) {
|
|
await VodRepository.insert({
|
|
type: "TOURNAMENT",
|
|
youtubeUrl: youtubeIdToYoutubeUrl("M4aV-BQWlVg"),
|
|
date: { day: 2, month: 2, year: 2023 },
|
|
submitterUserId: ADMIN_ID,
|
|
title: "LUTI Division X Tournament - ABBF (THRONE) vs. Ascension",
|
|
pov: {
|
|
type: "USER",
|
|
userId: faker.helpers.arrayElement(userIdsInRandomOrder()),
|
|
},
|
|
isValidated: true,
|
|
matches: [
|
|
{
|
|
mode: "SZ",
|
|
stageId: 8,
|
|
startsAt: secondsToHoursMinutesSecondString(13),
|
|
weapons: [3040],
|
|
},
|
|
{
|
|
mode: "CB",
|
|
stageId: 6,
|
|
startsAt: secondsToHoursMinutesSecondString(307),
|
|
weapons: [3040],
|
|
},
|
|
{
|
|
mode: "TC",
|
|
stageId: 2,
|
|
startsAt: secondsToHoursMinutesSecondString(680),
|
|
weapons: [3040],
|
|
},
|
|
{
|
|
mode: "SZ",
|
|
stageId: 9,
|
|
startsAt: secondsToHoursMinutesSecondString(1186),
|
|
weapons: [3040],
|
|
},
|
|
{
|
|
mode: "RM",
|
|
stageId: 2,
|
|
startsAt: secondsToHoursMinutesSecondString(1386),
|
|
weapons: [3000],
|
|
},
|
|
{
|
|
mode: "TC",
|
|
stageId: 4,
|
|
startsAt: secondsToHoursMinutesSecondString(1586),
|
|
weapons: [1110],
|
|
},
|
|
// there are other matches too...
|
|
],
|
|
});
|
|
}
|
|
}
|
|
|
|
async function realVideoCast() {
|
|
await VodRepository.insert({
|
|
type: "CAST",
|
|
youtubeUrl: youtubeIdToYoutubeUrl("M4aV-BQWlVg"),
|
|
date: { day: 2, month: 2, year: 2023 },
|
|
submitterUserId: ADMIN_ID,
|
|
title: "LUTI Division X Tournament - ABBF (THRONE) vs. Ascension",
|
|
isValidated: true,
|
|
matches: [
|
|
{
|
|
mode: "SZ",
|
|
stageId: 8,
|
|
startsAt: secondsToHoursMinutesSecondString(13),
|
|
weapons: [3040, 1000, 2000, 4000, 5000, 6000, 7010, 8000],
|
|
},
|
|
{
|
|
mode: "CB",
|
|
stageId: 6,
|
|
startsAt: secondsToHoursMinutesSecondString(307),
|
|
weapons: [3040, 1001, 2010, 4001, 5001, 6010, 7020, 8010],
|
|
},
|
|
{
|
|
mode: "TC",
|
|
stageId: 2,
|
|
startsAt: secondsToHoursMinutesSecondString(680),
|
|
weapons: [3040, 1010, 2020, 4010, 5010, 6020, 7010, 8000],
|
|
},
|
|
{
|
|
mode: "SZ",
|
|
stageId: 9,
|
|
startsAt: secondsToHoursMinutesSecondString(1186),
|
|
weapons: [3040, 1020, 2030, 4020, 5020, 6020, 7020, 8010],
|
|
},
|
|
// there are other matches too...
|
|
],
|
|
});
|
|
}
|
|
|
|
// some copy+paste from placements script
|
|
const addPlayerStm = sql.prepare(/* sql */ `
|
|
insert into "SplatoonPlayer" ("splId", "userId")
|
|
values (@splId, @userId)
|
|
on conflict ("splId") do nothing
|
|
`);
|
|
|
|
const addPlacementStm = sql.prepare(/* sql */ `
|
|
insert into "XRankPlacement" (
|
|
"weaponSplId",
|
|
"name",
|
|
"nameDiscriminator",
|
|
"power",
|
|
"rank",
|
|
"title",
|
|
"badges",
|
|
"bannerSplId",
|
|
"playerId",
|
|
"month",
|
|
"year",
|
|
"region",
|
|
"mode"
|
|
)
|
|
values (
|
|
@weaponSplId,
|
|
@name,
|
|
@nameDiscriminator,
|
|
@power,
|
|
@rank,
|
|
@title,
|
|
@badges,
|
|
@bannerSplId,
|
|
(select "id" from "SplatoonPlayer" where "splId" = @playerSplId),
|
|
@month,
|
|
@year,
|
|
@region,
|
|
@mode
|
|
)
|
|
`);
|
|
|
|
function xRankPlacements() {
|
|
sql.transaction(() => {
|
|
for (const [i, placement] of placements.entries()) {
|
|
const userId = () => {
|
|
// admin
|
|
if (placement.playerSplId === "qx6imlx72tfeqrhqfnmm") return ADMIN_ID;
|
|
// user in top 500 who is not plus server member
|
|
if (i === 0) return 500;
|
|
|
|
return null;
|
|
};
|
|
addPlayerStm.run({
|
|
splId: placement.playerSplId,
|
|
userId: userId(),
|
|
});
|
|
addPlacementStm.run(placement);
|
|
}
|
|
})();
|
|
}
|
|
|
|
const addArtStm = sql.prepare(/* sql */ `
|
|
insert into "Art" (
|
|
"imgId",
|
|
"authorId",
|
|
"isShowcase",
|
|
"description"
|
|
)
|
|
values (
|
|
@imgId,
|
|
@authorId,
|
|
@isShowcase,
|
|
@description
|
|
) returning *
|
|
`);
|
|
const addUnvalidatedUserSubmittedImageStm = sql.prepare(/* sql */ `
|
|
insert into "UnvalidatedUserSubmittedImage" (
|
|
"validatedAt",
|
|
"url",
|
|
"submitterUserId"
|
|
) values (
|
|
@validatedAt,
|
|
@url,
|
|
@submitterUserId
|
|
) returning *
|
|
`);
|
|
|
|
const teamAndTournamentImages = new Map<string, number>();
|
|
|
|
function insertTeamAndTournamentImages() {
|
|
for (const { filename } of SEED_TEAM_IMAGES) {
|
|
const result = addUnvalidatedUserSubmittedImageStm.get({
|
|
validatedAt: dateToDatabaseTimestamp(new Date()),
|
|
url: filename,
|
|
submitterUserId: ADMIN_ID,
|
|
}) as Tables["UserSubmittedImage"];
|
|
teamAndTournamentImages.set(filename, result.id);
|
|
}
|
|
|
|
for (const { filename } of SEED_TOURNAMENT_IMAGES) {
|
|
const result = addUnvalidatedUserSubmittedImageStm.get({
|
|
validatedAt: dateToDatabaseTimestamp(new Date()),
|
|
url: filename,
|
|
submitterUserId: ADMIN_ID,
|
|
}) as Tables["UserSubmittedImage"];
|
|
teamAndTournamentImages.set(filename, result.id);
|
|
}
|
|
}
|
|
|
|
function getTeamImageId(teamId: number): number | null {
|
|
const teamImage = SEED_TEAM_IMAGES.find((img) => img.teamId === teamId);
|
|
if (!teamImage) return null;
|
|
return teamAndTournamentImages.get(teamImage.filename) ?? null;
|
|
}
|
|
|
|
function getTournamentImageId(tournamentId: number): number | null {
|
|
const tournamentImage = SEED_TOURNAMENT_IMAGES.find(
|
|
(img) => img.tournamentId === tournamentId,
|
|
);
|
|
if (!tournamentImage) return null;
|
|
return teamAndTournamentImages.get(tournamentImage.filename) ?? null;
|
|
}
|
|
const addArtUserMetadataStm = sql.prepare(/* sql */ `
|
|
insert into "ArtUserMetadata" (
|
|
"artId",
|
|
"userId"
|
|
)
|
|
values (
|
|
@artId,
|
|
@userId
|
|
)
|
|
`);
|
|
const artImgFilenames = Array.from({ length: SEED_ART_URLS.length }, (_, i) =>
|
|
getArtFilename(i),
|
|
);
|
|
|
|
function arts() {
|
|
const artUsers = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17];
|
|
const allUsers = userIdsInRandomOrder();
|
|
const urls = [...artImgFilenames];
|
|
|
|
for (const userId of artUsers) {
|
|
for (let i = 0; i < faker.helpers.arrayElement([1, 2, 3, 3, 3, 4]); i++) {
|
|
const url = urls.pop()!;
|
|
if (!url) break;
|
|
|
|
const addedArt = addArtStm.get({
|
|
imgId: (
|
|
addUnvalidatedUserSubmittedImageStm.get({
|
|
validatedAt: dateToDatabaseTimestamp(new Date()),
|
|
url,
|
|
submitterUserId: userId,
|
|
}) as Tables["UserSubmittedImage"]
|
|
).id,
|
|
authorId: userId,
|
|
isShowcase: i === 0 ? 1 : 0,
|
|
description:
|
|
faker.number.float(1) > 0.5 ? faker.lorem.paragraph() : null,
|
|
}) as Tables["Art"];
|
|
|
|
if (i === 1) {
|
|
for (
|
|
let i = 0;
|
|
i < faker.helpers.arrayElement([1, 1, 1, 1, 2, 4]);
|
|
i++
|
|
) {
|
|
addArtUserMetadataStm.run({
|
|
artId: addedArt.id,
|
|
userId: i === 0 ? NZAP_TEST_ID : (allUsers.pop() ?? null),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const updateCommissionStm = sql.prepare(/* sql */ `
|
|
update "User"
|
|
set
|
|
"commissionsOpen" = @commissionsOpen,
|
|
"commissionText" = @commissionText
|
|
where id = @userId
|
|
`);
|
|
function commissionsOpen() {
|
|
const allUsers = userIdsInRandomOrder();
|
|
|
|
for (const userId of allUsers) {
|
|
if (faker.number.float(1) > 0.5) {
|
|
updateCommissionStm.run({
|
|
commissionsOpen: 1,
|
|
commissionText: faker.lorem.paragraph(),
|
|
userId,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const SENDOU_IN_FULL_GROUP = true;
|
|
async function groups() {
|
|
const users = userIdsInAscendingOrderById()
|
|
.slice(0, 100)
|
|
.filter((id) => id !== ADMIN_ID && id !== NZAP_TEST_ID);
|
|
users.push(NZAP_TEST_ID);
|
|
|
|
for (let i = 0; i < 25; i++) {
|
|
const group = await SQGroupRepository.createGroup({
|
|
status: "ACTIVE",
|
|
userId: users.pop()!,
|
|
});
|
|
|
|
const amountOfAdditionalMembers = () => {
|
|
if (SENDOU_IN_FULL_GROUP) {
|
|
if (i === 0) return 3;
|
|
if (i === 1) return 3;
|
|
}
|
|
|
|
return i === 0 ? 2 : i % 4;
|
|
};
|
|
|
|
for (let j = 0; j < amountOfAdditionalMembers(); j++) {
|
|
sql
|
|
.prepare(
|
|
/* sql */ `
|
|
insert into "GroupMember" ("groupId", "userId", "role")
|
|
values (@groupId, @userId, @role)
|
|
`,
|
|
)
|
|
.run({
|
|
groupId: group.id,
|
|
userId: users.pop()!,
|
|
role: "REGULAR",
|
|
});
|
|
}
|
|
|
|
if (i === 0 && SENDOU_IN_FULL_GROUP) {
|
|
users.push(ADMIN_ID);
|
|
}
|
|
}
|
|
}
|
|
|
|
const randomMapList = (
|
|
groupAlpha: number,
|
|
groupBravo: number,
|
|
): TournamentMapListMap[] => {
|
|
const szOnly = faker.helpers.arrayElement([true, false]);
|
|
|
|
let modePattern = faker.helpers
|
|
.shuffle([...modesShort])
|
|
.filter(() => faker.number.float(1) > 0.15);
|
|
if (modePattern.length === 0) {
|
|
modePattern = faker.helpers.shuffle([...rankedModesShort]);
|
|
}
|
|
|
|
const mapList: TournamentMapListMap[] = [];
|
|
const stageIdsShuffled = faker.helpers.shuffle([...stageIds]);
|
|
|
|
for (let i = 0; i < 7; i++) {
|
|
const mode = modePattern.pop()!;
|
|
mapList.push({
|
|
mode: szOnly ? "SZ" : mode,
|
|
stageId: stageIdsShuffled.pop()!,
|
|
source: i === 6 ? "BOTH" : i % 2 === 0 ? groupAlpha : groupBravo,
|
|
});
|
|
|
|
modePattern.unshift(mode);
|
|
}
|
|
|
|
return mapList;
|
|
};
|
|
|
|
const MATCHES_COUNT = 500;
|
|
|
|
const AMOUNT_OF_USERS_WITH_SKILLS = 100;
|
|
|
|
async function playedMatches() {
|
|
const _groupMembers = (() => {
|
|
return new Array(AMOUNT_OF_USERS_WITH_SKILLS).fill(null).map(() => {
|
|
const users = faker.helpers.shuffle(
|
|
userIdsInAscendingOrderById().slice(0, AMOUNT_OF_USERS_WITH_SKILLS),
|
|
);
|
|
|
|
return new Array(4).fill(null).map(() => users.pop()!);
|
|
});
|
|
})();
|
|
const defaultWeapons = Object.fromEntries(
|
|
userIdsInAscendingOrderById()
|
|
.slice(0, AMOUNT_OF_USERS_WITH_SKILLS)
|
|
.map((id) => {
|
|
const weapons = faker.helpers.shuffle([...mainWeaponIds]);
|
|
return [id, weapons[0]];
|
|
}),
|
|
);
|
|
|
|
let matchDate = new Date(Date.UTC(2023, 9, 15, 0, 0, 0, 0));
|
|
for (let i = 0; i < MATCHES_COUNT; i++) {
|
|
const groupMembers = faker.helpers.shuffle([..._groupMembers]);
|
|
const groupAlphaMembers = groupMembers.pop()!;
|
|
invariant(groupAlphaMembers, "groupAlphaMembers not found");
|
|
|
|
const getGroupBravo = (): number[] => {
|
|
const result = groupMembers.pop()!;
|
|
invariant(result, "groupBravoMembers not found");
|
|
if (groupAlphaMembers.some((m) => result.includes(m))) {
|
|
return getGroupBravo();
|
|
}
|
|
|
|
return result;
|
|
};
|
|
const groupBravoMembers = getGroupBravo();
|
|
|
|
let groupAlpha = 0;
|
|
let groupBravo = 0;
|
|
// -> create groups
|
|
for (let i = 0; i < 2; i++) {
|
|
const users = i === 0 ? [...groupAlphaMembers] : [...groupBravoMembers];
|
|
const group = await SQGroupRepository.createGroup({
|
|
status: "ACTIVE",
|
|
userId: users.pop()!,
|
|
});
|
|
|
|
// -> add regular members of groups
|
|
for (let i = 0; i < 3; i++) {
|
|
await SQGroupRepository.addMember(group.id, {
|
|
userId: users.pop()!,
|
|
});
|
|
}
|
|
|
|
if (i === 0) {
|
|
groupAlpha = group.id;
|
|
} else {
|
|
groupBravo = group.id;
|
|
}
|
|
}
|
|
|
|
invariant(groupAlpha !== 0 && groupBravo !== 0, "groups not created");
|
|
|
|
const match = await SQMatchRepository.create({
|
|
alphaGroupId: groupAlpha,
|
|
bravoGroupId: groupBravo,
|
|
mapList: randomMapList(groupAlpha, groupBravo),
|
|
memento: { users: {}, groups: {}, pools: [] },
|
|
});
|
|
|
|
// update match createdAt to the past
|
|
sql
|
|
.prepare(
|
|
/* sql */ `
|
|
update "GroupMatch"
|
|
set "createdAt" = @createdAt
|
|
where "id" = @id
|
|
`,
|
|
)
|
|
.run({
|
|
createdAt: dateToDatabaseTimestamp(matchDate),
|
|
id: match.id,
|
|
});
|
|
|
|
if (faker.number.float(1) > 0.95) {
|
|
// increment date by 1 day
|
|
matchDate = new Date(matchDate.getTime() + 1000 * 60 * 60 * 24);
|
|
}
|
|
|
|
// -> report score
|
|
const winners = faker.helpers.arrayElement([
|
|
["ALPHA", "ALPHA", "ALPHA", "ALPHA"],
|
|
["ALPHA", "ALPHA", "ALPHA", "BRAVO", "ALPHA"],
|
|
["BRAVO", "BRAVO", "BRAVO", "BRAVO"],
|
|
["ALPHA", "BRAVO", "BRAVO", "BRAVO", "BRAVO"],
|
|
["ALPHA", "ALPHA", "ALPHA", "BRAVO", "BRAVO", "BRAVO", "BRAVO"],
|
|
["BRAVO", "ALPHA", "BRAVO", "ALPHA", "BRAVO", "ALPHA", "BRAVO"],
|
|
["ALPHA", "BRAVO", "BRAVO", "ALPHA", "ALPHA", "ALPHA"],
|
|
["ALPHA", "BRAVO", "ALPHA", "BRAVO", "BRAVO", "BRAVO"],
|
|
]) as ("ALPHA" | "BRAVO")[];
|
|
const winner = winnersArrayToWinner(winners);
|
|
const finishedMatch = SendouQ.mapMatch(
|
|
(await SQMatchRepository.findById(match.id))!,
|
|
);
|
|
|
|
const { newSkills, differences } = calculateMatchSkills({
|
|
groupMatchId: match.id,
|
|
winner: winner === "ALPHA" ? groupAlphaMembers : groupBravoMembers,
|
|
loser: winner === "ALPHA" ? groupBravoMembers : groupAlphaMembers,
|
|
loserGroupId: winner === "ALPHA" ? groupBravo : groupAlpha,
|
|
winnerGroupId: winner === "ALPHA" ? groupAlpha : groupBravo,
|
|
});
|
|
|
|
const members = [
|
|
...finishedMatch.groupAlpha.members.map((m) => ({
|
|
...m,
|
|
groupId: match.alphaGroupId,
|
|
})),
|
|
...finishedMatch.groupBravo.members.map((m) => ({
|
|
...m,
|
|
groupId: match.bravoGroupId,
|
|
})),
|
|
];
|
|
sql.transaction(() => {
|
|
reportScore({
|
|
matchId: match.id,
|
|
reportedByUserId:
|
|
faker.number.float(1) > 0.5
|
|
? groupAlphaMembers[0]
|
|
: groupBravoMembers[0],
|
|
winners,
|
|
});
|
|
addSkills({
|
|
skills: newSkills,
|
|
differences,
|
|
groupMatchId: match.id,
|
|
oldMatchMemento: { users: {}, groups: {}, pools: [] },
|
|
});
|
|
setGroupAsInactive(groupAlpha);
|
|
setGroupAsInactive(groupBravo);
|
|
addMapResults(summarizeMaps({ match: finishedMatch, members, winners }));
|
|
addPlayerResults(
|
|
summarizePlayerResults({ match: finishedMatch, members, winners }),
|
|
);
|
|
})();
|
|
|
|
// -> add weapons for 90% of matches
|
|
if (faker.number.float(1) > 0.9) continue;
|
|
const users = [...groupAlphaMembers, ...groupBravoMembers];
|
|
const mapsWithUsers = users.flatMap((u) =>
|
|
finishedMatch.mapList.map((m) => ({ map: m, user: u })),
|
|
);
|
|
|
|
addReportedWeapons(
|
|
mapsWithUsers.map((mu) => {
|
|
const weapon = () => {
|
|
if (faker.number.float(1) < 0.9) return defaultWeapons[mu.user];
|
|
if (faker.number.float(1) > 0.5)
|
|
return (
|
|
mainWeaponIds.find((id) => id > defaultWeapons[mu.user]) ?? 0
|
|
);
|
|
|
|
const shuffled = faker.helpers.shuffle([...mainWeaponIds]);
|
|
|
|
return shuffled[0];
|
|
};
|
|
|
|
return {
|
|
groupMatchMapId: mu.map.id,
|
|
userId: mu.user,
|
|
weaponSplId: weapon(),
|
|
};
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
async function friendCodes() {
|
|
const allUsers = userIdsInRandomOrder();
|
|
|
|
for (const userId of allUsers) {
|
|
const friendCode = "####-####-####".replace(/#+/g, (m) =>
|
|
faker.string.numeric(m.length),
|
|
);
|
|
await UserRepository.insertFriendCode({
|
|
userId,
|
|
submitterUserId: userId,
|
|
friendCode,
|
|
});
|
|
}
|
|
}
|
|
|
|
async function lfgPosts() {
|
|
const allUsers = userIdsInRandomOrder(true).slice(0, 100);
|
|
|
|
allUsers.unshift(NZAP_TEST_ID);
|
|
|
|
for (const user of allUsers) {
|
|
await LFGRepository.insertPost({
|
|
authorId: user,
|
|
text: faker.lorem.paragraphs({ min: 1, max: 6 }),
|
|
timezone: faker.helpers.arrayElement(TIMEZONES),
|
|
type: faker.helpers.arrayElement(["PLAYER_FOR_TEAM", "COACH_FOR_TEAM"]),
|
|
});
|
|
}
|
|
|
|
await LFGRepository.insertPost({
|
|
authorId: ADMIN_ID,
|
|
text: faker.lorem.paragraphs({ min: 1, max: 6 }),
|
|
timezone: "Europe/Helsinki",
|
|
type: "TEAM_FOR_PLAYER",
|
|
teamId: 1,
|
|
});
|
|
}
|
|
|
|
async function scrimPosts() {
|
|
const allUsers = userIdsInRandomOrder(true);
|
|
|
|
// Only schedule admin's scrim at least 1 hour in the future, others can be 'now'
|
|
const date = (isAdmin = false) => {
|
|
if (isAdmin) {
|
|
const randomFuture = faker.date.between({
|
|
from: add(new Date(), { hours: 1 }),
|
|
to: add(new Date(), { days: 7 }),
|
|
});
|
|
randomFuture.setMinutes(0);
|
|
randomFuture.setSeconds(0);
|
|
randomFuture.setMilliseconds(0);
|
|
return dateToDatabaseTimestamp(randomFuture);
|
|
}
|
|
const isNow = faker.number.float(1) > 0.5;
|
|
if (isNow) {
|
|
return databaseTimestampNow();
|
|
}
|
|
const randomFuture = faker.date.between({
|
|
from: new Date(),
|
|
to: add(new Date(), { days: 7 }),
|
|
});
|
|
randomFuture.setMinutes(0);
|
|
randomFuture.setSeconds(0);
|
|
randomFuture.setMilliseconds(0);
|
|
return dateToDatabaseTimestamp(randomFuture);
|
|
};
|
|
|
|
const team = () => {
|
|
const hasTeam = faker.number.float(1) > 0.5;
|
|
|
|
if (!hasTeam) {
|
|
return null;
|
|
}
|
|
|
|
return faker.helpers.rangeToNumber({ min: 5, max: 49 });
|
|
};
|
|
|
|
const divRange = () => {
|
|
const hasDivRange = faker.number.float(1) > 0.2;
|
|
|
|
if (!hasDivRange) {
|
|
return null;
|
|
}
|
|
|
|
const maxDiv = faker.helpers.arrayElement([0, 1, 2, 3, 4, 5]);
|
|
const minDiv = faker.helpers.arrayElement([6, 7, 8, 9, 10, 11]);
|
|
|
|
return { maxDiv, minDiv };
|
|
};
|
|
|
|
const maps = (): "SZ" | "ALL" | "RANKED" | null => {
|
|
return faker.helpers.arrayElement(["SZ", "ALL", "RANKED", null, null]);
|
|
};
|
|
|
|
const users = () => {
|
|
const count = faker.helpers.arrayElement([4, 4, 4, 4, 4, 4, 5, 5, 5, 6]);
|
|
|
|
const result: Array<{ userId: number; isOwner: number }> = [];
|
|
for (let i = 0; i < count; i++) {
|
|
const user = allUsers.shift()!;
|
|
|
|
result.push({
|
|
userId: user,
|
|
isOwner: Number(i === 0),
|
|
});
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
const divs = divRange();
|
|
const atTime = date();
|
|
const hasRangeEnd = Math.random() > 0.5;
|
|
await ScrimPostRepository.insert({
|
|
at: atTime,
|
|
rangeEnd: hasRangeEnd
|
|
? dateToDatabaseTimestamp(
|
|
add(databaseTimestampToDate(atTime), {
|
|
hours: faker.helpers.rangeToNumber({ min: 1, max: 3 }),
|
|
}),
|
|
)
|
|
: null,
|
|
isScheduledForFuture: true,
|
|
maxDiv: divs?.maxDiv,
|
|
minDiv: divs?.minDiv,
|
|
teamId: team(),
|
|
text:
|
|
faker.number.float(1) > 0.5
|
|
? faker.lorem.sentences({ min: 1, max: 5 })
|
|
: null,
|
|
visibility: null,
|
|
users: users(),
|
|
managedByAnyone: true,
|
|
maps: maps(),
|
|
mapsTournamentId: null,
|
|
});
|
|
}
|
|
|
|
const adminPostAtTime = date(true); // admin's scrim is always at least 1 hour in the future
|
|
const adminPostId = await ScrimPostRepository.insert({
|
|
at: adminPostAtTime,
|
|
isScheduledForFuture: true,
|
|
text:
|
|
faker.number.float(1) > 0.5
|
|
? faker.lorem.sentences({ min: 1, max: 5 })
|
|
: null,
|
|
visibility: null,
|
|
users: users()
|
|
.map((u) => ({ ...u, isOwner: 0 }))
|
|
.concat({ userId: ADMIN_ID, isOwner: 1 }),
|
|
managedByAnyone: true,
|
|
maps: maps(),
|
|
mapsTournamentId: null,
|
|
});
|
|
await ScrimPostRepository.insertRequest({
|
|
scrimPostId: adminPostId,
|
|
users: users(),
|
|
message:
|
|
faker.number.float(1) > 0.5
|
|
? faker.lorem.sentence({ min: 5, max: 15 })
|
|
: null,
|
|
});
|
|
await ScrimPostRepository.insertRequest({
|
|
scrimPostId: adminPostId,
|
|
users: users(),
|
|
message:
|
|
faker.number.float(1) > 0.5
|
|
? faker.lorem.sentence({ min: 5, max: 15 })
|
|
: null,
|
|
});
|
|
}
|
|
|
|
async function scrimPostRequests() {
|
|
const allianceRogueMembers = await db
|
|
.selectFrom(["TeamMember"])
|
|
.select(["TeamMember.userId"])
|
|
.where("TeamMember.teamId", "=", 1)
|
|
.execute();
|
|
|
|
for (const id of [1, 5, 12, 14, 19]) {
|
|
await ScrimPostRepository.insertRequest({
|
|
scrimPostId: id,
|
|
users: allianceRogueMembers.map((member) => ({
|
|
userId: member.userId,
|
|
isOwner: member.userId === ADMIN_ID ? 1 : 0,
|
|
})),
|
|
teamId: 1,
|
|
message:
|
|
faker.number.float(1) > 0.5
|
|
? faker.lorem.sentence({ min: 5, max: 15 })
|
|
: null,
|
|
});
|
|
}
|
|
|
|
await ScrimPostRepository.acceptRequest(3);
|
|
}
|
|
|
|
async function associations() {
|
|
const allUsers = userIdsInRandomOrder(true);
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
await AssociationRepository.insert({
|
|
name: faker.company.name(),
|
|
userId: i === 2 ? allUsers.shift()! : ADMIN_ID,
|
|
});
|
|
|
|
for (
|
|
let j = 0;
|
|
j < faker.helpers.arrayElement([4, 6, 8, 10, 12, 24, 32]);
|
|
j++
|
|
) {
|
|
await AssociationRepository.addMember({
|
|
associationId: i + 1,
|
|
userId: i === 2 && j === 0 ? ADMIN_ID : allUsers.shift()!,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
async function notifications() {
|
|
const values: 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: 1,
|
|
tournamentName: "PICNIC #2",
|
|
tournamentTeamId: 1,
|
|
},
|
|
},
|
|
{
|
|
type: "TO_BRACKET_STARTED",
|
|
meta: {
|
|
tournamentId: 1,
|
|
tournamentName: "PICNIC #2",
|
|
bracketIdx: 0,
|
|
bracketName: "Groups Stage",
|
|
},
|
|
},
|
|
{
|
|
type: "BADGE_ADDED",
|
|
meta: { badgeName: "In The Zone 20-29", badgeId: 39 },
|
|
},
|
|
{
|
|
type: "TAGGED_TO_ART",
|
|
meta: {
|
|
adderUsername: "N-ZAP",
|
|
adderDiscordId: NZAP_TEST_DISCORD_ID,
|
|
artId: 1, // does not exist
|
|
},
|
|
},
|
|
{
|
|
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: 1, tournamentName: "PICNIC #2" },
|
|
pictureUrl:
|
|
"http://localhost:5173/static-assets/img/tournament-logos/pn.png",
|
|
},
|
|
];
|
|
|
|
for (const [i, value] of values.entries()) {
|
|
await NotificationRepository.insert(value, [
|
|
{
|
|
userId: ADMIN_ID,
|
|
seen: i <= 7 ? 1 : 0,
|
|
},
|
|
]);
|
|
await NotificationRepository.insert(value, [
|
|
{
|
|
userId: NZAP_TEST_ID,
|
|
seen: i <= 7 ? 1 : 0,
|
|
},
|
|
]);
|
|
}
|
|
|
|
const createdAts = [
|
|
sub(new Date(), { days: 10 }),
|
|
sub(new Date(), { days: 8 }),
|
|
sub(new Date(), { days: 5, hours: 2 }),
|
|
sub(new Date(), { days: 4, minutes: 30 }),
|
|
sub(new Date(), { days: 3, hours: 2 }),
|
|
sub(new Date(), { days: 3, hours: 1, minutes: 10 }),
|
|
sub(new Date(), { days: 2, hours: 5 }),
|
|
sub(new Date(), { minutes: 10 }),
|
|
sub(new Date(), { minutes: 5 }),
|
|
];
|
|
|
|
invariant(
|
|
values.length - 1 === createdAts.length,
|
|
"values and createdAts length mismatch",
|
|
);
|
|
|
|
for (let i = 0; i < values.length - 1; i++) {
|
|
sql
|
|
.prepare(
|
|
/* sql */ `
|
|
update "Notification"
|
|
set "createdAt" = @createdAt
|
|
where "id" = @id
|
|
`,
|
|
)
|
|
.run({
|
|
createdAt: dateToDatabaseTimestamp(createdAts[i]),
|
|
id: i + 1,
|
|
});
|
|
}
|
|
}
|
|
|
|
async function organization() {
|
|
await TournamentOrganizationRepository.create({
|
|
ownerId: ADMIN_ID,
|
|
name: "sendou.ink",
|
|
});
|
|
|
|
await TournamentOrganizationRepository.update({
|
|
id: 1,
|
|
name: "sendou.ink",
|
|
description: "Sendou.ink official tournaments",
|
|
socials: [
|
|
"https://bsky.app/profile/sendou.ink",
|
|
"https://twitch.tv/sendou",
|
|
],
|
|
members: [
|
|
{
|
|
userId: ADMIN_ID,
|
|
role: "ADMIN",
|
|
roleDisplayName: null,
|
|
},
|
|
{
|
|
userId: NZAP_TEST_ID,
|
|
role: "MEMBER",
|
|
roleDisplayName: null,
|
|
},
|
|
{
|
|
userId: 3,
|
|
role: "ADMIN",
|
|
roleDisplayName: null,
|
|
},
|
|
],
|
|
series: [],
|
|
badges: [],
|
|
});
|
|
}
|