diff --git a/.gitignore b/.gitignore index f0b58870d..1ca480e25 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ node_modules /public/build .env +notes.md + db*.sqlite3* dump diff --git a/app/db/index.ts b/app/db/index.ts index 9b690a1de..69ed79a04 100644 --- a/app/db/index.ts +++ b/app/db/index.ts @@ -3,6 +3,7 @@ import * as plusSuggestions from "./models/plusSuggestions/queries.server"; import * as plusVotes from "./models/plusVotes/queries.server"; import * as badges from "./models/badges/queries.server"; import * as calendarEvents from "./models/calendar/queries.server"; +import * as builds from "./models/builds/queries.server"; export const db = { users, @@ -10,4 +11,5 @@ export const db = { plusVotes, badges, calendarEvents, + builds, }; diff --git a/app/db/models/builds/createBuild.sql b/app/db/models/builds/createBuild.sql new file mode 100644 index 000000000..2d9ec99fd --- /dev/null +++ b/app/db/models/builds/createBuild.sql @@ -0,0 +1,20 @@ +insert into + "Build" ( + "ownerId", + "title", + "description", + "modes", + "headGearSplId", + "clothesGearSplId", + "shoesGearSplId" + ) +values + ( + @ownerId, + @title, + @description, + @modes, + @headGearSplId, + @clothesGearSplId, + @shoesGearSplId + ) returning * \ No newline at end of file diff --git a/app/db/models/builds/createBuildAbility.sql b/app/db/models/builds/createBuildAbility.sql new file mode 100644 index 000000000..de45f02b7 --- /dev/null +++ b/app/db/models/builds/createBuildAbility.sql @@ -0,0 +1,4 @@ +insert into + "BuildAbility" ("buildId", "gearType", "ability", "slotIndex") +values + (@buildId, @gearType, @ability, @slotIndex) \ No newline at end of file diff --git a/app/db/models/builds/createBuildWeapon.sql b/app/db/models/builds/createBuildWeapon.sql new file mode 100644 index 000000000..4e728bcd8 --- /dev/null +++ b/app/db/models/builds/createBuildWeapon.sql @@ -0,0 +1,4 @@ +insert into + "BuildWeapon" ("buildId", "weaponSplId") +values + (@buildId, @weaponSplId) \ No newline at end of file diff --git a/app/db/models/builds/queries.server.ts b/app/db/models/builds/queries.server.ts new file mode 100644 index 000000000..354041eb1 --- /dev/null +++ b/app/db/models/builds/queries.server.ts @@ -0,0 +1,53 @@ +import { sql } from "~/db/sql"; +import type { Build, BuildAbility, BuildWeapon } from "~/db/types"; +import type { Ability, ModeShort } from "~/modules/in-game-lists"; +import createBuildSql from "./createBuild.sql"; +import createBuildWeaponSql from "./createBuildWeapon.sql"; +import createBuildAbilitySql from "./createBuildAbility.sql"; + +const createBuildStm = sql.prepare(createBuildSql); +const createBuildWeaponStm = sql.prepare(createBuildWeaponSql); +const createBuildAbilityStm = sql.prepare(createBuildAbilitySql); + +interface CreateArgs { + ownerId: Build["ownerId"]; + title: Build["title"]; + description: Build["description"]; + modes: Array | null; + headGearSplId: Build["headGearSplId"]; + clothesGearSplId: Build["clothesGearSplId"]; + shoesGearSplId: Build["shoesGearSplId"]; + weaponSplIds: Array; + abilities: Array<{ + gearType: BuildAbility["gearType"]; + ability: Ability; + slotIndex: BuildAbility["slotIndex"]; + }>; +} +export const create = sql.transaction((build: CreateArgs) => { + const createdBuild = createBuildStm.get({ + ownerId: build.ownerId, + title: build.title, + description: build.description, + modes: build.modes?.join(",") ?? null, + headGearSplId: build.headGearSplId, + clothesGearSplId: build.clothesGearSplId, + shoesGearSplId: build.shoesGearSplId, + }) as Build; + + for (const weaponSplId of build.weaponSplIds) { + createBuildWeaponStm.run({ + buildId: createdBuild.id, + weaponSplId, + }); + } + + for (const { gearType, ability, slotIndex } of build.abilities) { + createBuildAbilityStm.run({ + buildId: createdBuild.id, + gearType, + ability, + slotIndex, + }); + } +}); diff --git a/app/db/seed.ts b/app/db/seed.ts index c2732f5ba..a4c72e523 100644 --- a/app/db/seed.ts +++ b/app/db/seed.ts @@ -1,17 +1,25 @@ import { faker } from "@faker-js/faker"; +import capitalize from "just-capitalize"; +import shuffle from "just-shuffle"; import invariant from "tiny-invariant"; +import { ADMIN_DISCORD_ID } from "~/constants"; +import { db } from "~/db"; +import { sql } from "~/db/sql"; +import { + abilityCodes, + clothesGearIds, + headGearIds, + modesShort, + shoesGearIds, + weaponIds, +} from "~/modules/in-game-lists"; import { lastCompletedVoting, nextNonCompletedVoting, } from "~/modules/plus-server"; -import { db } from "~/db"; -import { sql } from "~/db/sql"; -import type { UpsertManyPlusVotesArgs } from "./models/plusVotes/queries.server"; -import { ADMIN_DISCORD_ID } from "~/constants"; -import shuffle from "just-shuffle"; -import { dateToDatabaseTimestamp } from "~/utils/dates"; -import capitalize from "just-capitalize"; import allTags from "~/routes/calendar/tags.json"; +import { dateToDatabaseTimestamp } from "~/utils/dates"; +import type { UpsertManyPlusVotesArgs } from "./models/plusVotes/queries.server"; const ADMIN_TEST_AVATAR = "e424e1ba50d2019fdc4730d261e56c55"; @@ -36,6 +44,7 @@ const basicSeeds = [ calendarEvents, calendarEventBadges, calendarEventResults, + adminBuilds, ]; export function seed() { @@ -486,3 +495,63 @@ function calendarEventResults() { userIds = userIdsInRandomOrder(); } } + +function adminBuilds() { + for (let i = 0; i < 50; i++) { + const randomOrderHeadGear = shuffle(headGearIds.slice()); + const randomOrderClothesGear = shuffle(clothesGearIds.slice()); + const randomOrderShoesGear = shuffle(shoesGearIds.slice()); + const randomOrderWeaponIds = shuffle(weaponIds.slice()); + + db.builds.create({ + title: `${capitalize(faker.word.adjective())} ${capitalize( + faker.word.noun() + )}`, + ownerId: 1, + description: Math.random() < 0.75 ? faker.lorem.paragraph() : null, + clothesGearSplId: randomOrderHeadGear[0]!, + headGearSplId: randomOrderClothesGear[0]!, + shoesGearSplId: randomOrderShoesGear[0]!, + weaponSplIds: new Array( + faker.helpers.arrayElement([1, 1, 1, 2, 2, 3, 4, 5, 6]) + ) + .fill(null) + .map(() => randomOrderWeaponIds.pop()!), + modes: + Math.random() < 0.75 + ? modesShort.filter(() => Math.random() < 0.5) + : null, + abilities: new Array(12).fill(null).map((_, i) => { + const gearType = i < 4 ? "HEAD" : i < 8 ? "CLOTHES" : "SHOES"; + + const randomOrderAbilities = shuffle([...abilityCodes]); + + const getAbility = () => { + const legalAbilityForSlot = randomOrderAbilities.find((ability) => { + if (ability.type === "HEAD_ONLY" && gearType !== "HEAD") { + return false; + } + if (ability.type === "CLOTHES_ONLY" && gearType !== "CLOTHES") { + return false; + } + if (ability.type === "SHOES_ONLY" && gearType !== "SHOES") { + return false; + } + + return true; + }); + + invariant(legalAbilityForSlot); + + return legalAbilityForSlot.name; + }; + + return { + ability: getAbility(), + gearType, + slotIndex: (i % 4) as any, + }; + }), + }); + } +} diff --git a/app/db/types.ts b/app/db/types.ts index 829a3a2fa..118c86433 100644 --- a/app/db/types.ts +++ b/app/db/types.ts @@ -1,3 +1,4 @@ +import type { Ability } from "~/modules/in-game-lists"; import type allTags from "../routes/calendar/tags.json"; export interface User { @@ -117,3 +118,27 @@ export interface CalendarEventBadge { eventId: number; badgeId: number; } + +export interface Build { + id: number; + ownerId: number; + title: string; + description: string | null; + modes: string | null; + headGearSplId: number; + clothesGearSplId: number; + shoesGearSplId: number; + updatedAt: number; +} + +export interface BuildWeapon { + buildId: number; + weaponSplId: number; +} + +export interface BuildAbility { + buildId: number; + gearType: "HEAD" | "CLOTHES" | "SHOES"; + ability: Ability; + slotIndex: 0 | 1 | 2 | 3; +} diff --git a/app/modules/in-game-lists/abilities.ts b/app/modules/in-game-lists/abilities.ts index fce975ea6..18c4856ef 100644 --- a/app/modules/in-game-lists/abilities.ts +++ b/app/modules/in-game-lists/abilities.ts @@ -1,12 +1,4 @@ -export type AbilityType = - | "STACKABLE" - | "HEAD_ONLY" - | "CLOTHES_ONLY" - | "SHOES_ONLY"; - -export const abilityCodes: Readonly< - Array<{ name: string; type: AbilityType }> -> = [ +export const abilityCodes = [ { name: "AD", type: "CLOTHES_ONLY" }, { name: "BRU", type: "STACKABLE" }, { name: "CB", type: "HEAD_ONLY" }, @@ -34,3 +26,5 @@ export const abilityCodes: Readonly< { name: "T", type: "CLOTHES_ONLY" }, { name: "TI", type: "CLOTHES_ONLY" }, ] as const; + +export const abilitiesShort = abilityCodes.map((ability) => ability.name); diff --git a/app/modules/in-game-lists/index.ts b/app/modules/in-game-lists/index.ts index 957fa86fe..b5acecfa2 100644 --- a/app/modules/in-game-lists/index.ts +++ b/app/modules/in-game-lists/index.ts @@ -2,4 +2,5 @@ export { stages } from "./stages"; export { modes, modesShort } from "./modes"; export { weaponIds } from "./weapon-ids"; export { headGearIds, clothesGearIds, shoesGearIds } from "./gear-ids"; -export type { ModeShort, Stage } from "./types"; +export { abilitiesShort, abilityCodes } from "./abilities"; +export type { Ability, AbilityType, ModeShort, Stage } from "./types"; diff --git a/app/modules/in-game-lists/types.ts b/app/modules/in-game-lists/types.ts index 1eed57426..aed557ef6 100644 --- a/app/modules/in-game-lists/types.ts +++ b/app/modules/in-game-lists/types.ts @@ -1,6 +1,10 @@ +import type { abilityCodes } from "./abilities"; import type { modes } from "./modes"; import type { stages } from "./stages"; export type ModeShort = typeof modes[number]["short"]; export type Stage = typeof stages[number]; + +export type Ability = typeof abilityCodes[number]["name"]; +export type AbilityType = typeof abilityCodes[number]["type"]; diff --git a/migrations/007-builds.js b/migrations/007-builds.js new file mode 100644 index 000000000..1cc4fada4 --- /dev/null +++ b/migrations/007-builds.js @@ -0,0 +1,48 @@ +module.exports.up = function (db) { + db.prepare( + ` + create table "Build" ( + "id" integer primary key, + "ownerId" integer not null, + "title" text not null, + "description" text, + "modes" text, + "headGearSplId" integer not null, + "clothesGearSplId" integer not null, + "shoesGearSplId" integer not null, + "updatedAt" integer default (strftime('%s', 'now')) not null, + foreign key ("ownerId") references "User"("id") on delete restrict + ) strict + ` + ).run(); + + db.prepare( + ` + create table "BuildWeapon" ( + "buildId" integer not null, + "weaponSplId" integer not null, + foreign key ("buildId") references "Build"("id") on delete cascade, + unique("buildId", "weaponSplId") on conflict rollback + ) strict + ` + ).run(); + + db.prepare( + ` + create table "BuildAbility" ( + "buildId" integer not null, + "gearType" text not null, + "ability" text not null, + "slotIndex" integer not null, + foreign key ("buildId") references "Build"("id") on delete cascade, + unique("buildId", "gearType", "slotIndex") on conflict rollback + ) strict + ` + ).run(); +}; + +module.exports.down = function (db) { + for (const table of ["Build", "BuildWeapon", "BuildAbility"]) { + db.prepare(`drop table "${table}"`).run(); + } +};