diff --git a/app/admin/[character]/[page]/page.tsx b/app/admin/[character]/[page]/page.tsx index a60ffa8..cbbd8bf 100644 --- a/app/admin/[character]/[page]/page.tsx +++ b/app/admin/[character]/[page]/page.tsx @@ -10,6 +10,7 @@ import { AdminHeader } from "@/components/admin/admin-header"; import { EditorPresence } from "@/components/admin/editor-presence"; import { Button } from "@/components/ui/button"; import { getAdminSession } from "@/lib/auth/server"; +import { toTeamDraft } from "@/lib/guides/team-draft"; import { getCatalogOptions, getAdminGuideContent, @@ -135,18 +136,7 @@ export default async function StructuredEditorPage({ })), teams: content.teams.map((team) => ({ id: team.id, - title: team.title, - rotationSeconds: team.rotationSeconds, - combo: team.combo, - members: team.members.map((member) => ({ - characterKey: member.characterKey, - weaponKey: member.weaponKey, - artifactKey: member.artifactKey, - secondArtifactKey: member.secondArtifactKey, - constellation: member.constellation, - refinement: member.refinement, - damage: member.damage, - })), + ...toTeamDraft(team), })), teamGroups: content.teamGroups.map((group) => ({ id: group.id, @@ -156,18 +146,7 @@ export default async function StructuredEditorPage({ iconUrl: group.iconUrl, teams: group.teams.map((team) => ({ id: team.id, - title: team.title, - rotationSeconds: team.rotationSeconds, - combo: team.combo, - members: team.members.map((member) => ({ - characterKey: member.characterKey, - weaponKey: member.weaponKey, - artifactKey: member.artifactKey, - secondArtifactKey: member.secondArtifactKey, - constellation: member.constellation, - refinement: member.refinement, - damage: member.damage, - })), + ...toTeamDraft(team), })), })), extras: content.extras.filter((section) => !extraSection || section.id === extraSection.id).map((section) => ({ diff --git a/components/admin/constellation-image-generator.tsx b/components/admin/constellation-image-generator.tsx index 50c327a..a6bb491 100644 --- a/components/admin/constellation-image-generator.tsx +++ b/components/admin/constellation-image-generator.tsx @@ -36,6 +36,7 @@ import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; +import { isValidPercent } from "@/lib/guides/percent-input"; const rowSchema = z.object({ level: z.number().int().min(1).max(6), @@ -69,11 +70,6 @@ const parseConstellationDraft = (value: unknown): ConstellationDraft | null => { return parsed.success ? parsed.data : null; }; -function validPercent(value: string): boolean { - const number = Number(value); - return value.trim().length > 0 && Number.isFinite(number) && number >= -9999 && number <= 9999; -} - export function ConstellationImageGenerator({ characters, }: { @@ -85,7 +81,7 @@ export function ConstellationImageGenerator({ parseConstellationDraft, ); const character = characters.find((item) => item.key === draft.characterKey); - const valid = Boolean(character && draft.rows.every((row) => validPercent(row.cumulativePercent))); + const valid = Boolean(character && draft.rows.every((row) => isValidPercent(row.cumulativePercent))); const guide = useMemo(() => ({ coverUrl: character?.imageUrl ?? "", @@ -153,7 +149,7 @@ export function ConstellationImageGenerator({ {draft.rows.map((row, index) => ( - +
C{row.level} @@ -163,7 +159,7 @@ export function ConstellationImageGenerator({ type="number" step="0.01" value={row.cumulativePercent} - aria-invalid={!validPercent(row.cumulativePercent)} + aria-invalid={!isValidPercent(row.cumulativePercent)} onChange={(event) => setDraft((current) => ({ ...current, rows: current.rows.map((item, rowIndex) => rowIndex === index diff --git a/components/admin/guide-section-editor.tsx b/components/admin/guide-section-editor.tsx index 7d13471..07ac11c 100644 --- a/components/admin/guide-section-editor.tsx +++ b/components/admin/guide-section-editor.tsx @@ -170,6 +170,7 @@ import { elementBackgroundColor, elementIconFilter } from "@/lib/catalog/element import { supportsWeaponType } from "@/lib/catalog/lunaris"; import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity"; import { isCustomTravelerKey } from "@/lib/catalog/travelers"; +import { toTeamDraft } from "@/lib/guides/team-draft"; import { applyPresetFields, applyTeamFields, @@ -3194,20 +3195,7 @@ function TeamEditor({ }; const importEverywhereTeams = (sourceTeams: TeamEverywhereImport[]) => { if (!sourceTeams.length) return; - const importedTeams = sourceTeams.map((team) => ({ - title: team.title, - rotationSeconds: team.rotationSeconds, - combo: team.combo, - members: team.members.map((member) => ({ - characterKey: member.characterKey, - weaponKey: member.weaponKey, - artifactKey: member.artifactKey, - secondArtifactKey: member.secondArtifactKey, - constellation: member.constellation, - refinement: member.refinement, - damage: member.damage, - })), - })); + const importedTeams = sourceTeams.map(toTeamDraft); if (!appendImportedTeams(importedTeams)) return; setEverywhereImportOpen(false); setEverywhereSelectedIds(new Set()); @@ -4243,20 +4231,7 @@ function TeamEditor({ checkboxId={`guide-team-${team.id}`} onCheckedChange={(checked) => setSelectedImports((current) => { if (checked) { - return { ...current, [team.id]: { - title: team.title, - rotationSeconds: team.rotationSeconds, - combo: team.combo, - members: team.members.map((member) => ({ - characterKey: member.characterKey, - weaponKey: member.weaponKey, - artifactKey: member.artifactKey, - secondArtifactKey: member.secondArtifactKey, - constellation: member.constellation, - refinement: member.refinement, - damage: member.damage, - })), - } }; + return { ...current, [team.id]: toTeamDraft(team) }; } const next = { ...current }; delete next[team.id]; diff --git a/components/admin/stygian-editor.tsx b/components/admin/stygian-editor.tsx index 0f112ff..13717ca 100644 --- a/components/admin/stygian-editor.tsx +++ b/components/admin/stygian-editor.tsx @@ -2,12 +2,10 @@ import Image from "@/components/ui/resilient-image"; import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { useEffect, useMemo, useRef, useState, useTransition, type ReactNode } from "react"; +import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { GripVerticalIcon, PlusIcon, - RefreshCwIcon, Trash2Icon, } from "lucide-react"; import { @@ -29,11 +27,9 @@ import { import { CSS } from "@dnd-kit/utilities"; import { toast } from "sonner"; -import { - saveStygianRecommendationsAction, - syncStygianArchiveAction, -} from "@/app/admin/stygian/actions"; +import { saveStygianRecommendationsAction } from "@/app/admin/stygian/actions"; import { AutosaveStatus } from "@/components/admin/autosave-status"; +import { StygianSyncButton } from "@/components/admin/stygian-sync-button"; import { ArtifactSelect, CatalogDropdown, @@ -70,10 +66,10 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Spinner } from "@/components/ui/spinner"; import { elementIconFilter } from "@/lib/catalog/element"; import { AutosaveCoordinator, type AutosaveStatus as CoordinatorStatus } from "@/lib/editor/autosave"; import { supportsWeaponType } from "@/lib/catalog/lunaris"; +import { stygianBossPositionLabels, stygianBossPositions, stygianLevels } from "@/lib/stygian/constants"; import { stygianElements, type StygianRecommendationItemInput, @@ -1021,11 +1017,9 @@ export function StygianEditor({ weapons: CatalogItem[]; artifacts: CatalogItem[]; }) { - const router = useRouter(); const [version, setVersion] = useState(schedule.version); const [level, setLevel] = useState<105 | 110>(110); const [bossPosition, setBossPosition] = useState(0); - const [pending, startTransition] = useTransition(); return (
setLevel(Number(next) as 105 | 110)} > - Level 105 - Level 110 + {stygianLevels.map((entry) => ( + Level {entry} + ))} - {([105, 110] as const).map((entry) => ( + {stygianLevels.map((entry) => ( setBossPosition(Number(next))} > - {[0, 1, 2].map((position) => { + {stygianBossPositions.map((position) => { const boss = bosses.find( (candidate) => candidate.level === entry && candidate.position === position, ); const label = boss?.name ?? `Boss ${position + 1}`; - const positionLabel = ["บน", "กลาง", "ล่าง"][position]; + const positionLabel = stygianBossPositionLabels[position]; return ( , "size"> = {}) { const router = useRouter(); const [pending, startTransition] = useTransition(); return ( -
diff --git a/components/public/stygian-guide.tsx b/components/public/stygian-guide.tsx index 1e54d1c..73523fa 100644 --- a/components/public/stygian-guide.tsx +++ b/components/public/stygian-guide.tsx @@ -9,6 +9,7 @@ import { import { elementIconFilter } from "@/lib/catalog/element"; import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity"; import type { getActiveCatalog } from "@/lib/guides/queries"; +import { stygianBossPositionLabels, stygianBossPositions, stygianLevels } from "@/lib/stygian/constants"; import type { getStygianSchedule } from "@/lib/stygian/repository"; import { cn } from "@/lib/utils"; @@ -103,23 +104,24 @@ export function StygianGuide({ schedule, catalog }: { schedule: Schedule; catalo return ( - Level 105 - Level 110 + {stygianLevels.map((level) => ( + Level {level} + ))} - {([105, 110] as const).map((level) => ( + {stygianLevels.map((level) => ( - {[0, 1, 2].map((position) => { + {stygianBossPositions.map((position) => { const boss = schedule.bosses.find((candidate) => candidate.level === level && candidate.position === position); const label = boss?.name ?? `Boss ${position + 1}`; - const positionLabel = ["บน", "กลาง", "ล่าง"][position]; + const positionLabel = stygianBossPositionLabels[position]; return ; })} - {[0, 1, 2].map((position) => )} + {stygianBossPositions.map((position) => )} ))} diff --git a/components/stygian/stygian-boss-display.tsx b/components/stygian/stygian-boss-display.tsx index ac63256..3ab34e6 100644 --- a/components/stygian/stygian-boss-display.tsx +++ b/components/stygian/stygian-boss-display.tsx @@ -11,6 +11,7 @@ import { import { Card, CardContent } from "@/components/ui/card"; import { LunarisText } from "@/components/public/lunaris-text"; import { parseLunarisText } from "@/lib/catalog/lunaris"; +import { stygianBossPositionLabels } from "@/lib/stygian/constants"; export type StygianBossDisplay = { name: string; @@ -71,7 +72,7 @@ export function StygianBossTab({ position: number; priority?: boolean; }) { - const positionLabel = ["บน", "กลาง", "ล่าง"][position]; + const positionLabel = stygianBossPositionLabels[position]; return ( <> {boss ? ( diff --git a/lib/guides/percent-input.test.ts b/lib/guides/percent-input.test.ts index 4e226b0..7c9a562 100644 --- a/lib/guides/percent-input.test.ts +++ b/lib/guides/percent-input.test.ts @@ -1,8 +1,29 @@ import { describe, expect, it } from "vitest"; -import { finalizePercentInput, normalizePercentInput } from "@/lib/guides/percent-input"; +import { finalizePercentInput, isValidPercent, normalizePercentInput } from "@/lib/guides/percent-input"; describe("percentage input", () => { + it.each([ + ["-9999", true], + ["9999", true], + ["0", true], + ["12.5", true], + ["-12.5", true], + [" 100 ", true], + ["1e2", true], + ["", false], + [" \t\n", false], + ["-", false], + ["invalid", false], + ["NaN", false], + ["Infinity", false], + ["-Infinity", false], + ["9999.01", false], + ["-9999.01", false], + ])("validates %j as %s", (value, valid) => { + expect(isValidPercent(value)).toBe(valid); + }); + it("turns only a lone dash into zero when editing finishes", () => { expect(normalizePercentInput("-")).toBe("-"); expect(finalizePercentInput("-")).toBe("0"); diff --git a/lib/guides/percent-input.ts b/lib/guides/percent-input.ts index 65866cf..09d4ea0 100644 --- a/lib/guides/percent-input.ts +++ b/lib/guides/percent-input.ts @@ -1,5 +1,11 @@ const DECIMAL = /^-?(?:\d+\.?\d*|\.\d*)?$/u; +export function isValidPercent(value: string): boolean { + if (!value.trim()) return false; + const number = Number(value); + return Number.isFinite(number) && number >= -9999 && number <= 9999; +} + export function normalizePercentInput(nextValue: string, currentValue = ""): string { const value = nextValue.trim(); return DECIMAL.test(value) ? value : currentValue; diff --git a/lib/guides/queries.test.ts b/lib/guides/queries.test.ts index e2fcef7..65c3539 100644 --- a/lib/guides/queries.test.ts +++ b/lib/guides/queries.test.ts @@ -27,9 +27,9 @@ vi.mock("@/db", () => ({ import { artifactConditions, artifactProfiles, constellationConditions, extraSectionRows, guides, guideSections, teamCompositions, teamConditions, teamMembers, weaponConditions, - catalogMaterials, guidePosterBleeding, media, + catalogCharacters, catalogMaterials, guidePosterBleeding, media, } from "@/db/schema"; -import { getAdminGuideContent, listAdminTeamImportsForCharacter } from "./queries"; +import { getActiveCatalog, getAdminGuideContent, listAdminTeamImportsForCharacter } from "./queries"; beforeEach(() => { mocks.reads.length = 0; @@ -103,6 +103,48 @@ describe("admin section reads", () => { }); }); +describe("character material metadata", () => { + it.each(["catalog", "overview"] as const)( + "preserves ascension and leveling metadata in the %s read", + async (read) => { + const character = { + key: "test", imageKey: "character", constellations: [], talents: [], + ascension: [{ type: "ascension", items: [{ key: "known", amount: 2 }, { key: "missing/key", amount: 3 }] }], + leveling: [{ type: "leveling", items: [{ key: "missing/key", amount: 5 }, { key: "known", amount: 7 }] }], + }; + const original = structuredClone(character); + mocks.rows.set(catalogCharacters, [character]); + mocks.rows.set(guides, [{ + guide: { id: "guide", slug: "test", posterArtMediaId: null }, + cover: { objectKey: "cover", status: "ready" }, + character, + }]); + mocks.rows.set(catalogMaterials, [{ + key: "known", name: "Known material", description: "Description", + rarity: 4, type: "upgrade", imageKey: "material.webp", + }]); + + const result = read === "catalog" + ? (await getActiveCatalog())?.characters[0] + : (await getAdminGuideContent("test", "overview"))?.character; + const known = { + key: "known", name: "Known material", description: "Description", + rarity: 4, type: "upgrade", imageUrl: "/media/material.webp", + lunarisUrl: "https://lunaris.moe/material/known", available: true, + }; + const missing = { + key: "missing/key", name: "missing/key", description: null, + rarity: null, type: null, imageUrl: null, + lunarisUrl: "https://lunaris.moe/material/missing%2Fkey", available: false, + }; + expect(result?.ascension).toEqual([{ type: "ascension", items: [{ ...known, amount: 2 }, { ...missing, amount: 3 }] }]); + expect(result?.leveling).toEqual([{ type: "leveling", items: [{ ...missing, amount: 5 }, { ...known, amount: 7 }] }]); + expect(mocks.reads.filter((table) => table === catalogMaterials)).toHaveLength(1); + expect(character).toEqual(original); + }, + ); +}); + describe("team import lookup", () => { it("hydrates matching teams and source labels with one member read", async () => { mocks.rows.set(teamCompositions, [{ diff --git a/lib/guides/queries.ts b/lib/guides/queries.ts index 427e32c..bb659e3 100644 --- a/lib/guides/queries.ts +++ b/lib/guides/queries.ts @@ -58,6 +58,22 @@ function catalogCharacterWithUrls( item: typeof catalogCharacters.$inferSelect, materialByKey = new Map(), ) { + const materialWithMetadata = ( + entry: typeof item.ascension[number]["items"][number], + ) => { + const material = materialByKey.get(entry.key); + return { + ...entry, + name: material?.name ?? entry.key, + description: material?.description ?? null, + rarity: material?.rarity ?? null, + type: material?.type ?? null, + imageUrl: material ? publicMediaUrl(material.imageKey) : null, + lunarisUrl: `https://lunaris.moe/material/${encodeURIComponent(entry.key)}`, + available: Boolean(material), + }; + }; + return { ...item, imageUrl: publicMediaUrl(item.imageKey), @@ -74,35 +90,11 @@ function catalogCharacterWithUrls( })), ascension: item.ascension.map((group) => ({ ...group, - items: group.items.map((entry) => { - const material = materialByKey.get(entry.key); - return { - ...entry, - name: material?.name ?? entry.key, - description: material?.description ?? null, - rarity: material?.rarity ?? null, - type: material?.type ?? null, - imageUrl: material ? publicMediaUrl(material.imageKey) : null, - lunarisUrl: `https://lunaris.moe/material/${encodeURIComponent(entry.key)}`, - available: Boolean(material), - }; - }), + items: group.items.map(materialWithMetadata), })), leveling: item.leveling.map((group) => ({ ...group, - items: group.items.map((entry) => { - const material = materialByKey.get(entry.key); - return { - ...entry, - name: material?.name ?? entry.key, - description: material?.description ?? null, - rarity: material?.rarity ?? null, - type: material?.type ?? null, - imageUrl: material ? publicMediaUrl(material.imageKey) : null, - lunarisUrl: `https://lunaris.moe/material/${encodeURIComponent(entry.key)}`, - available: Boolean(material), - }; - }), + items: group.items.map(materialWithMetadata), })), }; } diff --git a/lib/guides/team-draft.test.ts b/lib/guides/team-draft.test.ts new file mode 100644 index 0000000..9759d48 --- /dev/null +++ b/lib/guides/team-draft.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from "vitest"; + +import { teamSectionSchema } from "@/lib/guides/schemas"; +import { toTeamDraft } from "@/lib/guides/team-draft"; +import { cloneTeamImports } from "@/lib/guides/team-tools"; + +const storedTeam = () => ({ + id: "stored-team", + title: "Example team", + rotationSeconds: "21.5", + combo: "E > Q", + guideName: "Source guide", + conditionName: "Boss", + members: [1, 2, 3, 4].map((index) => ({ + characterKey: `character-${index}`, + weaponKey: `weapon-${index}`, + artifactKey: `artifact-${index}`, + secondArtifactKey: index === 1 ? "artifact-second" : null, + constellation: index === 1 ? "0-6" : "2", + refinement: index === 1 ? "1-5" : "4", + damage: "123456.78", + characterName: `Character ${index}`, + character: { name: `Character ${index}`, imageUrl: "/character.webp" }, + })), +}); + +describe("team drafts", () => { + it("selects editable fields and excludes persisted IDs and hydrated metadata", () => { + const source = storedTeam(); + const draft = toTeamDraft(source); + + expect(draft).toEqual({ + title: "Example team", + rotationSeconds: "21.5", + combo: "E > Q", + members: [1, 2, 3, 4].map((index) => ({ + characterKey: `character-${index}`, + weaponKey: `weapon-${index}`, + artifactKey: `artifact-${index}`, + secondArtifactKey: index === 1 ? "artifact-second" : null, + constellation: index === 1 ? "0-6" : "2", + refinement: index === 1 ? "1-5" : "4", + damage: "123456.78", + })), + }); + expect({ id: source.id, ...draft }).toHaveProperty("id", "stored-team"); + }); + + it("creates independent draft members and preserves nullable equipment", () => { + const source = storedTeam(); + const member = { ...source.members[0], weaponKey: null, artifactKey: null }; + const input = { ...source, members: [member] }; + const draft = toTeamDraft(input); + const otherDraft = toTeamDraft(input); + + expect(draft.members[0]).toMatchObject({ weaponKey: null, artifactKey: null }); + expect(draft.members).not.toBe(input.members); + expect(draft.members[0]).not.toBe(member); + draft.members[0].damage = "0"; + expect(member.damage).toBe("123456.78"); + expect(otherDraft.members[0].damage).toBe("123456.78"); + }); + + it("imports teams with fresh IDs and produces a valid editor save payload", () => { + const source = storedTeam(); + const draft = toTeamDraft(source); + const [imported] = cloneTeamImports([draft], () => "new-team"); + expect(imported.clientId).toBe("new-team"); + expect(imported.members[0]).not.toBe(draft.members[0]); + + const result = teamSectionSchema.parse({ + expectedVersion: 7, + groups: [{ + name: "Boss", + note: null, + iconMediaId: null, + teams: [{ + title: imported.title, + rotationSeconds: Number(imported.rotationSeconds), + combo: imported.combo, + members: imported.members.map((member) => ({ + ...member, + damage: Number(member.damage), + })), + }], + }], + }); + expect(result.expectedVersion).toBe(7); + expect(result.groups[0].teams[0].rotationSeconds).toBe(21.5); + expect(result.groups[0].teams[0].members[0]).toMatchObject({ + constellation: "0-6", refinement: "1-5", damage: 123456.78, + }); + }); +}); diff --git a/lib/guides/team-draft.ts b/lib/guides/team-draft.ts new file mode 100644 index 0000000..3f19320 --- /dev/null +++ b/lib/guides/team-draft.ts @@ -0,0 +1,20 @@ +import type { TeamToolValue } from "@/lib/guides/team-tools"; + +export function toTeamDraft( + team: Omit, +): Omit { + return { + title: team.title, + rotationSeconds: team.rotationSeconds, + combo: team.combo, + members: team.members.map((member) => ({ + characterKey: member.characterKey, + weaponKey: member.weaponKey, + artifactKey: member.artifactKey, + secondArtifactKey: member.secondArtifactKey, + constellation: member.constellation, + refinement: member.refinement, + damage: member.damage, + })), + }; +} diff --git a/lib/stygian/constants.ts b/lib/stygian/constants.ts new file mode 100644 index 0000000..82a4d2c --- /dev/null +++ b/lib/stygian/constants.ts @@ -0,0 +1,3 @@ +export const stygianLevels = [105, 110] as const; +export const stygianBossPositions = [0, 1, 2] as const; +export const stygianBossPositionLabels = ["บน", "กลาง", "ล่าง"] as const; diff --git a/lib/stygian/repository.ts b/lib/stygian/repository.ts index de6bddb..6947fea 100644 --- a/lib/stygian/repository.ts +++ b/lib/stygian/repository.ts @@ -80,25 +80,23 @@ export async function syncStygianArchive() { } await db.transaction(async (tx) => { - await tx.insert(stygianSchedules).values({ - scheduleId, + const scheduleValues = { challengeName: parsed.data.challengeName, scheduleStartTime: lunarisDate(parsed.data.scheduleStartTime), scheduleEndTime: lunarisDate(parsed.data.scheduleEndTime), + }; + await tx.insert(stygianSchedules).values({ + scheduleId, + ...scheduleValues, }).onConflictDoUpdate({ target: stygianSchedules.scheduleId, set: { - challengeName: parsed.data.challengeName, - scheduleStartTime: lunarisDate(parsed.data.scheduleStartTime), - scheduleEndTime: lunarisDate(parsed.data.scheduleEndTime), + ...scheduleValues, updatedAt: new Date(), }, }); for (const boss of bosses) { - await tx.insert(stygianBosses).values({ - scheduleId, - level: boss.level, - position: boss.position, + const bossValues = { configId: boss.configId, name: boss.name, hp: String(boss.hp), @@ -107,18 +105,15 @@ export async function syncStygianArchive() { recommendedMechanics: boss.recommendedMechanics, recommendedMechanicTypes: boss.recommendedMechanicTypes, imageKey: imageKeyByName.get(boss.imageName)!, + }; + await tx.insert(stygianBosses).values({ + scheduleId, + level: boss.level, + position: boss.position, + ...bossValues, }).onConflictDoUpdate({ target: [stygianBosses.scheduleId, stygianBosses.level, stygianBosses.position], - set: { - configId: boss.configId, - name: boss.name, - hp: String(boss.hp), - description: boss.description, - mechanics: boss.mechanics, - recommendedMechanics: boss.recommendedMechanics, - recommendedMechanicTypes: boss.recommendedMechanicTypes, - imageKey: imageKeyByName.get(boss.imageName)!, - }, + set: bossValues, }); } }); diff --git a/lib/stygian/schema.ts b/lib/stygian/schema.ts index 28ceb0e..4db399c 100644 --- a/lib/stygian/schema.ts +++ b/lib/stygian/schema.ts @@ -1,6 +1,9 @@ import * as z from "zod"; -export const stygianLevels = [105, 110] as const; +import { stygianLevels } from "@/lib/stygian/constants"; + +export { stygianLevels } from "@/lib/stygian/constants"; + export const stygianElements = ["Anemo", "Geo", "Electro", "Dendro", "Hydro", "Pyro", "Cryo"] as const; const levelConfigSchema = z.object({