feat : update UI
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
|||||||
SortableContext,
|
SortableContext,
|
||||||
sortableKeyboardCoordinates,
|
sortableKeyboardCoordinates,
|
||||||
useSortable,
|
useSortable,
|
||||||
|
verticalListSortingStrategy,
|
||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -742,6 +743,52 @@ function VisualRecommendationGrid({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SortableTeam({
|
||||||
|
id,
|
||||||
|
position,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
position: number;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({ id });
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={{ transform: CSS.Transform.toString(transform), transition }}
|
||||||
|
className={cn(isDragging && "relative z-10 opacity-60")}
|
||||||
|
>
|
||||||
|
<Card size="sm">
|
||||||
|
<CardContent className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-sm font-medium">Team {position + 1}</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-xs"
|
||||||
|
className="touch-none"
|
||||||
|
aria-label={`Drag to reorder team ${position + 1}`}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
<GripVerticalIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function BossRecommendationEditor({
|
function BossRecommendationEditor({
|
||||||
scheduleId,
|
scheduleId,
|
||||||
version,
|
version,
|
||||||
@@ -776,6 +823,13 @@ function BossRecommendationEditor({
|
|||||||
.filter((group) => group.kind === "unit")
|
.filter((group) => group.kind === "unit")
|
||||||
.flatMap((group) => group.items.flatMap((item) => toInput(item) ?? [])),
|
.flatMap((group) => group.items.flatMap((item) => toInput(item) ?? [])),
|
||||||
);
|
);
|
||||||
|
const teamSensors = useSensors(
|
||||||
|
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
||||||
|
useSensor(KeyboardSensor, {
|
||||||
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const teamIds = teams.map((_, index) => `team-${index}`);
|
||||||
const fingerprint = JSON.stringify({ teams, units });
|
const fingerprint = JSON.stringify({ teams, units });
|
||||||
const valid = teams.every(
|
const valid = teams.every(
|
||||||
(team) =>
|
(team) =>
|
||||||
@@ -868,9 +922,28 @@ function BossRecommendationEditor({
|
|||||||
<div className="flex min-w-0 flex-col gap-6">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
<FieldSet className="gap-3">
|
<FieldSet className="gap-3">
|
||||||
<FieldLegend>Recommended team</FieldLegend>
|
<FieldLegend>Recommended team</FieldLegend>
|
||||||
{teams.map((team, teamIndex) => (
|
<DndContext
|
||||||
<Card key={teamIndex} size="sm">
|
sensors={teamSensors}
|
||||||
<CardContent className="flex flex-col gap-2">
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={({ active, over }) => {
|
||||||
|
if (!over || active.id === over.id) return;
|
||||||
|
const from = teamIds.indexOf(String(active.id));
|
||||||
|
const to = teamIds.indexOf(String(over.id));
|
||||||
|
if (from >= 0 && to >= 0)
|
||||||
|
setTeams((current) => arrayMove(current, from, to));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={teamIds}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{teams.map((team, teamIndex) => (
|
||||||
|
<SortableTeam
|
||||||
|
key={teamIds[teamIndex]}
|
||||||
|
id={teamIds[teamIndex]}
|
||||||
|
position={teamIndex}
|
||||||
|
>
|
||||||
<VisualRecommendationGrid
|
<VisualRecommendationGrid
|
||||||
items={team}
|
items={team}
|
||||||
limit={4}
|
limit={4}
|
||||||
@@ -908,9 +981,11 @@ function BossRecommendationEditor({
|
|||||||
<Trash2Icon data-icon="inline-start" />
|
<Trash2Icon data-icon="inline-start" />
|
||||||
Remove team
|
Remove team
|
||||||
</DeleteConfirmationDialog>
|
</DeleteConfirmationDialog>
|
||||||
</CardContent>
|
</SortableTeam>
|
||||||
</Card>
|
))}
|
||||||
))}
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export function SiteHeader({ editHref = "/admin", guideId }: { editHref?: string
|
|||||||
nativeButton={false}
|
nativeButton={false}
|
||||||
render={<Link href="/stygian" />}
|
render={<Link href="/stygian" />}
|
||||||
>
|
>
|
||||||
|
<Image src="/icon/stygian.webp" alt="" width={24} height={24} className="size-6 object-contain" />
|
||||||
Stygian Guide
|
Stygian Guide
|
||||||
</Button>
|
</Button>
|
||||||
<VisitorPresence guideId={guideId} />
|
<VisitorPresence guideId={guideId} />
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { CheckIcon, XIcon } from "lucide-react";
|
||||||
|
|
||||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -16,6 +17,24 @@ type Schedule = NonNullable<Awaited<ReturnType<typeof getStygianSchedule>>>;
|
|||||||
type Catalog = NonNullable<Awaited<ReturnType<typeof getActiveCatalog>>>;
|
type Catalog = NonNullable<Awaited<ReturnType<typeof getActiveCatalog>>>;
|
||||||
type Item = Schedule["recommendations"][number]["items"][number];
|
type Item = Schedule["recommendations"][number]["items"][number];
|
||||||
|
|
||||||
|
function RecommendedMechanic({ mechanic, type }: { mechanic: string; type: string }) {
|
||||||
|
const parts = mechanic.split(/(\{SPRITE_PRESET#\d+\})/gu);
|
||||||
|
return (
|
||||||
|
<span className="inline-flex flex-wrap items-center gap-1">
|
||||||
|
{parts.map((part, index) => {
|
||||||
|
const preset = /^\{SPRITE_PRESET#(\d+)\}$/u.exec(part)?.[1];
|
||||||
|
if (preset) return <Image key={`${part}-${index}`} src={`https://api.lunaris.moe/data/assets/icons/${preset}.png`} alt="" width={20} height={20} className="size-5 shrink-0 object-contain" />;
|
||||||
|
return part ? <LunarisText key={`${part}-${index}`} nodes={parseLunarisText(part)} fallback={part} /> : null;
|
||||||
|
})}
|
||||||
|
{type === "Disadvantage"
|
||||||
|
? <XIcon className="size-4 text-red-500" aria-hidden="true" />
|
||||||
|
: type === "Advantage"
|
||||||
|
? <CheckIcon className="size-4 text-emerald-500" aria-hidden="true" />
|
||||||
|
: null}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function recommendation(item: Item, catalog: Catalog) {
|
function recommendation(item: Item, catalog: Catalog) {
|
||||||
if (item.kind === "element" && item.element) {
|
if (item.kind === "element" && item.element) {
|
||||||
const source = catalog.characters.find((character) => character.element === item.element && character.elementIconUrl);
|
const source = catalog.characters.find((character) => character.element === item.element && character.elementIconUrl);
|
||||||
@@ -45,27 +64,29 @@ function RecommendationIcon({ item, catalog, teamLayout = false }: { item: Item;
|
|||||||
if (item.kind === "character") {
|
if (item.kind === "character") {
|
||||||
const hasEquipment = Boolean(value.weapon || value.artifact);
|
const hasEquipment = Boolean(value.weapon || value.artifact);
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 flex-col gap-1 rounded-lg border bg-card p-1">
|
<div className={cn("flex min-w-0 flex-col gap-1 rounded-lg border bg-card p-1", teamLayout && "sm:p-2")}>
|
||||||
<div className={teamLayout && hasEquipment ? "grid grid-cols-3 gap-1" : "flex flex-col gap-1"}>
|
<div className={teamLayout ? "flex flex-col gap-1 sm:grid sm:grid-cols-3 sm:gap-2" : "flex flex-col gap-1"}>
|
||||||
<div className={`${teamLayout && hasEquipment ? "col-span-2 row-span-2" : ""} relative aspect-square overflow-hidden rounded-lg ${rarityGradientClass(value.rarity)}`}>
|
<div className={cn("relative aspect-square overflow-hidden rounded-lg", teamLayout && "sm:col-span-2 sm:row-span-2", rarityGradientClass(value.rarity))}>
|
||||||
{value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes="128px" className="object-contain" /> : null}
|
{value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes={teamLayout ? "(min-width: 640px) 128px, 25vw" : "128px"} className="object-contain" /> : null}
|
||||||
</div>
|
</div>
|
||||||
{hasEquipment ? <div className={teamLayout ? "grid grid-cols-1 gap-1" : "grid grid-cols-2 gap-1"}>
|
{hasEquipment ? <div className={teamLayout ? "grid grid-cols-2 gap-1 sm:row-span-2 sm:grid-cols-1 sm:gap-2" : "grid grid-cols-2 gap-1"}>
|
||||||
{value.weapon ? <div className={cn("relative aspect-square overflow-hidden rounded-md border", rarityGradientClass(value.weapon.rarity))}><Image src={value.weapon.imageUrl} alt={value.weapon.name} fill sizes="64px" className="object-contain p-1" /></div> : null}
|
{value.weapon ? <div className={cn("relative aspect-square overflow-hidden rounded-md border", rarityGradientClass(value.weapon.rarity))}><Image src={value.weapon.imageUrl} alt={value.weapon.name} fill sizes="64px" className="object-contain p-1" /></div> : null}
|
||||||
{value.artifact ? <div className={cn("relative aspect-square overflow-hidden rounded-md border", rarityGradientClass(value.artifact.rarity))}><Image src={value.artifact.imageUrl} alt={value.artifact.name} fill sizes="64px" className="object-contain p-1" /></div> : null}
|
{value.artifact ? <div className={cn("relative aspect-square overflow-hidden rounded-md border", rarityGradientClass(value.artifact.rarity))}><Image src={value.artifact.imageUrl} alt={value.artifact.name} fill sizes="64px" className="object-contain p-1" /></div> : null}
|
||||||
</div> : null}
|
</div> : teamLayout ? <div className="hidden sm:block" aria-hidden="true" /> : null}
|
||||||
</div>
|
</div>
|
||||||
{teamLayout ? <div className="grid grid-cols-2 gap-1"><Badge variant="secondary" className="justify-center">{value.badge === "Character" ? "C—" : value.badge}</Badge><Badge variant="secondary" className="justify-center">{value.weapon && value.refinement ? `R${value.refinement}` : "R—"}</Badge></div> : <div className="flex min-w-0 items-center justify-between gap-1"><span className="truncate text-xs font-medium" title={value.name}>{value.name}</span><Badge variant="secondary">{value.badge}</Badge></div>}
|
{teamLayout ? item.constellationLevel !== null || value.weapon && value.refinement ? <div className="flex justify-center gap-1">
|
||||||
|
{item.constellationLevel !== null ? <Badge variant="secondary">C{item.constellationLevel}</Badge> : null}
|
||||||
|
{value.weapon && value.refinement ? <Badge variant="secondary">R{value.refinement}</Badge> : null}
|
||||||
|
</div> : null : <span className="truncate text-center text-xs font-medium" title={value.name}>{value.name}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 flex-col items-center gap-1 text-center">
|
<div className="flex min-w-0 flex-col items-center gap-1 text-center">
|
||||||
<div className={cn("relative size-16 overflow-hidden rounded-lg border", item.kind !== "element" && rarityGradientClass(value.rarity))}>
|
<div className={cn("relative aspect-square w-full overflow-hidden rounded-lg border", item.kind !== "element" && rarityGradientClass(value.rarity))}>
|
||||||
{value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes="64px" className="object-contain" style={item.kind === "element" && item.element ? { filter: elementIconFilter(item.element) } : undefined} /> : null}
|
{value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes="80px" className="object-contain" style={item.kind === "element" && item.element ? { filter: elementIconFilter(item.element) } : undefined} /> : null}
|
||||||
</div>
|
</div>
|
||||||
<span className="max-w-20 truncate text-xs font-medium" title={value.name}>{value.name}</span>
|
<span className="max-w-20 truncate text-xs font-medium" title={value.name}>{value.name}</span>
|
||||||
<Badge variant="outline" className="capitalize">{value.badge}</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -78,14 +99,11 @@ function BossCard({ schedule, level, position, catalog }: { schedule: Schedule;
|
|||||||
const units = groups.filter((group) => group.kind === "unit").flatMap((group) => group.items);
|
const units = groups.filter((group) => group.kind === "unit").flatMap((group) => group.items);
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="grid gap-6 md:grid-cols-[14rem_minmax(0,1fr)]">
|
<CardContent className="flex flex-col gap-6">
|
||||||
<aside className="self-start">
|
<header className="text-center">
|
||||||
<div className="relative aspect-square overflow-hidden rounded-xl bg-muted/30">
|
<h2 className="font-heading text-lg font-semibold">{boss.name}</h2>
|
||||||
<Image src={boss.imageUrl} alt={boss.name} fill sizes="224px" className="object-contain p-3" priority={level === 110} />
|
|
||||||
</div>
|
|
||||||
<h2 className="mt-3 font-heading text-lg font-semibold">{boss.name}</h2>
|
|
||||||
<p className="mt-1 text-sm text-muted-foreground">Level {level} · HP {Math.round(boss.hp).toLocaleString("en-US")}</p>
|
<p className="mt-1 text-sm text-muted-foreground">Level {level} · HP {Math.round(boss.hp).toLocaleString("en-US")}</p>
|
||||||
</aside>
|
</header>
|
||||||
<div className="flex min-w-0 flex-col gap-5">
|
<div className="flex min-w-0 flex-col gap-5">
|
||||||
<Accordion>
|
<Accordion>
|
||||||
<AccordionItem value="overview">
|
<AccordionItem value="overview">
|
||||||
@@ -106,15 +124,15 @@ function BossCard({ schedule, level, position, catalog }: { schedule: Schedule;
|
|||||||
<section className="flex flex-col gap-3" aria-label={`Recommended teams for ${boss.name}`}>
|
<section className="flex flex-col gap-3" aria-label={`Recommended teams for ${boss.name}`}>
|
||||||
<h3 className="font-heading text-lg font-semibold">Recommended team</h3>
|
<h3 className="font-heading text-lg font-semibold">Recommended team</h3>
|
||||||
{teams.length ? teams.map((team) => (
|
{teams.length ? teams.map((team) => (
|
||||||
<div key={team.id} className="grid grid-cols-[repeat(4,minmax(0,7.5rem))] gap-2 rounded-lg border bg-muted/20 p-2">
|
<div key={team.id} className="grid w-full max-w-xl grid-cols-4 items-start gap-1 rounded-lg border bg-muted/20 p-2 sm:gap-3">
|
||||||
{team.items.slice(0, 4).map((item) => <RecommendationIcon key={`${item.groupId}-${item.position}`} item={item} catalog={catalog} teamLayout />)}
|
{team.items.slice(0, 4).map((item) => <RecommendationIcon key={`${item.groupId}-${item.position}`} item={item} catalog={catalog} teamLayout />)}
|
||||||
{Array.from({ length: Math.max(0, 4 - team.items.length) }, (_, index) => <div key={`empty-${index}`} className="aspect-[3/4] rounded-lg border border-dashed bg-muted/30" aria-hidden="true" />)}
|
{Array.from({ length: Math.max(0, 4 - team.items.length) }, (_, index) => <div key={`empty-${index}`} className="aspect-[4/3] rounded-lg border border-dashed bg-muted/30" aria-hidden="true" />)}
|
||||||
</div>
|
</div>
|
||||||
)) : <p className="text-sm text-muted-foreground">No team recommendations yet.</p>}
|
)) : <p className="text-sm text-muted-foreground">No team recommendations yet.</p>}
|
||||||
</section>
|
</section>
|
||||||
<section className="flex flex-col gap-3" aria-label={`Recommended units for ${boss.name}`}>
|
<section className="flex flex-col gap-3" aria-label={`Recommended units for ${boss.name}`}>
|
||||||
<h3 className="font-heading text-lg font-semibold">Recommended unit</h3>
|
<h3 className="font-heading text-lg font-semibold">Recommended unit</h3>
|
||||||
{units.length ? <div className="grid grid-cols-[repeat(auto-fill,minmax(6rem,7.5rem))] gap-2">{units.map((item) => <RecommendationIcon key={`${item.groupId}-${item.position}`} item={item} catalog={catalog} />)}</div> : <p className="text-sm text-muted-foreground">No unit recommendations yet.</p>}
|
{units.length ? <div className="grid grid-cols-[repeat(auto-fill,minmax(4rem,5rem))] gap-2">{units.map((item) => <RecommendationIcon key={`${item.groupId}-${item.position}`} item={item} catalog={catalog} />)}</div> : <p className="text-sm text-muted-foreground">No unit recommendations yet.</p>}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -132,10 +150,18 @@ export function StygianGuide({ schedule, catalog }: { schedule: Schedule; catalo
|
|||||||
{([105, 110] as const).map((level) => (
|
{([105, 110] as const).map((level) => (
|
||||||
<TabsContent key={level} value={String(level)}>
|
<TabsContent key={level} value={String(level)}>
|
||||||
<Tabs defaultValue="0">
|
<Tabs defaultValue="0">
|
||||||
<TabsList variant="line" className="grid h-10 w-full grid-cols-3 overflow-hidden" aria-label={`Level ${level} bosses`}>
|
<TabsList variant="line" className="grid w-full grid-cols-3 overflow-hidden group-data-horizontal/tabs:h-auto group-data-horizontal/tabs:min-h-96" aria-label={`Level ${level} bosses`}>
|
||||||
{[0, 1, 2].map((position) => {
|
{[0, 1, 2].map((position) => {
|
||||||
const boss = schedule.bosses.find((candidate) => candidate.level === level && candidate.position === position);
|
const boss = schedule.bosses.find((candidate) => candidate.level === level && candidate.position === position);
|
||||||
return <TabsTrigger key={position} value={String(position)} className="h-full min-w-0 overflow-hidden rounded-none px-2"><span className="block min-w-0 truncate">{boss?.name ?? `Boss ${position + 1}`}</span></TabsTrigger>;
|
const label = boss?.name ?? `Boss ${position + 1}`;
|
||||||
|
const positionLabel = ["บน", "กลาง", "ล่าง"][position];
|
||||||
|
return <TabsTrigger key={position} value={String(position)} aria-label={`${positionLabel}: ${label}`} title={label} className="h-full min-w-0 flex-col justify-start overflow-hidden rounded-none px-2 py-3">
|
||||||
|
{boss ? <Image src={boss.imageUrl} alt="" width={256} height={256} sizes="256px" className="size-64 shrink-0 object-contain" priority={level === 110} /> : <span>{position + 1}</span>}
|
||||||
|
<span className="text-base font-semibold">{positionLabel}</span>
|
||||||
|
{boss?.recommendedMechanics.some(Boolean) ? <span className="mt-2 flex flex-col items-center gap-1 text-xs font-normal whitespace-normal text-muted-foreground">
|
||||||
|
{boss.recommendedMechanics.map((mechanic, index) => mechanic ? <RecommendedMechanic key={`${mechanic}-${index}`} mechanic={mechanic} type={boss.recommendedMechanicTypes[index] ?? (index === 0 ? "Advantage" : index === 1 ? "Disadvantage" : "None")} /> : null)}
|
||||||
|
</span> : null}
|
||||||
|
</TabsTrigger>;
|
||||||
})}
|
})}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{[0, 1, 2].map((position) => <TabsContent key={position} value={String(position)} className="pt-4"><BossCard schedule={schedule} level={level} position={position} catalog={catalog} /></TabsContent>)}
|
{[0, 1, 2].map((position) => <TabsContent key={position} value={String(position)} className="pt-4"><BossCard schedule={schedule} level={level} position={position} catalog={catalog} /></TabsContent>)}
|
||||||
|
|||||||
@@ -301,6 +301,8 @@ export const stygianBosses = guidesSchema.table(
|
|||||||
hp: numeric("hp", { precision: 18, scale: 2 }).notNull(),
|
hp: numeric("hp", { precision: 18, scale: 2 }).notNull(),
|
||||||
description: text("description").notNull(),
|
description: text("description").notNull(),
|
||||||
mechanics: jsonb("mechanics").$type<StygianMechanic[]>().default([]).notNull(),
|
mechanics: jsonb("mechanics").$type<StygianMechanic[]>().default([]).notNull(),
|
||||||
|
recommendedMechanics: jsonb("recommended_mechanics").$type<string[]>().default([]).notNull(),
|
||||||
|
recommendedMechanicTypes: jsonb("recommended_mechanic_types").$type<string[]>().default([]).notNull(),
|
||||||
imageKey: text("image_key").notNull(),
|
imageKey: text("image_key").notNull(),
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "guides"."stygian_boss" ADD COLUMN "recommended_mechanics" jsonb DEFAULT '[]'::jsonb NOT NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "guides"."stygian_boss" ADD COLUMN "recommended_mechanic_types" jsonb DEFAULT '[]'::jsonb NOT NULL;
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -428,6 +428,20 @@
|
|||||||
"when": 1790769279233,
|
"when": 1790769279233,
|
||||||
"tag": "0060_lovely_texas_twister",
|
"tag": "0060_lovely_texas_twister",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 61,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1790782934903,
|
||||||
|
"tag": "0061_wooden_hedge_knight",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 62,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1790785825070,
|
||||||
|
"tag": "0062_large_catseye",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -104,6 +104,8 @@ export async function syncStygianArchive() {
|
|||||||
hp: String(boss.hp),
|
hp: String(boss.hp),
|
||||||
description: boss.description,
|
description: boss.description,
|
||||||
mechanics: boss.mechanics,
|
mechanics: boss.mechanics,
|
||||||
|
recommendedMechanics: boss.recommendedMechanics,
|
||||||
|
recommendedMechanicTypes: boss.recommendedMechanicTypes,
|
||||||
imageKey: imageKeyByName.get(boss.imageName)!,
|
imageKey: imageKeyByName.get(boss.imageName)!,
|
||||||
}).onConflictDoUpdate({
|
}).onConflictDoUpdate({
|
||||||
target: [stygianBosses.scheduleId, stygianBosses.level, stygianBosses.position],
|
target: [stygianBosses.scheduleId, stygianBosses.level, stygianBosses.position],
|
||||||
@@ -113,6 +115,8 @@ export async function syncStygianArchive() {
|
|||||||
hp: String(boss.hp),
|
hp: String(boss.hp),
|
||||||
description: boss.description,
|
description: boss.description,
|
||||||
mechanics: boss.mechanics,
|
mechanics: boss.mechanics,
|
||||||
|
recommendedMechanics: boss.recommendedMechanics,
|
||||||
|
recommendedMechanicTypes: boss.recommendedMechanicTypes,
|
||||||
imageKey: imageKeyByName.get(boss.imageName)!,
|
imageKey: imageKeyByName.get(boss.imageName)!,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ function level(monsterLevel: number) {
|
|||||||
enMonsterSpecialDesc: `Description ${index}`,
|
enMonsterSpecialDesc: `Description ${index}`,
|
||||||
enMonsterSpecialMechanicName: ["Mechanic", "", "Missing description"],
|
enMonsterSpecialMechanicName: ["Mechanic", "", "Missing description"],
|
||||||
enLevelMaxDescription: ["Do the thing", "Ignored", ""],
|
enLevelMaxDescription: ["Do the thing", "Ignored", ""],
|
||||||
|
enRecommendedLevelMechanic: ["{SPRITE_PRESET#11003} characters", "", " {SPRITE_PRESET#11002} characters "],
|
||||||
|
tagType: ["Advantage", "None", "Disadvantage"],
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -41,6 +43,8 @@ describe("Stygian Lunaris parsing", () => {
|
|||||||
name: "Boss 0",
|
name: "Boss 0",
|
||||||
hp: 1234.56,
|
hp: 1234.56,
|
||||||
mechanics: [{ name: "Mechanic", description: "Do the thing" }],
|
mechanics: [{ name: "Mechanic", description: "Do the thing" }],
|
||||||
|
recommendedMechanics: ["{SPRITE_PRESET#11003} characters", "", "{SPRITE_PRESET#11002} characters"],
|
||||||
|
recommendedMechanicTypes: ["Advantage", "None", "Disadvantage"],
|
||||||
imageName: "UI_Img_LeyLineChallenge_Boss_0",
|
imageName: "UI_Img_LeyLineChallenge_Boss_0",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const levelConfigSchema = z.object({
|
|||||||
enMonsterSpecialDesc: z.string().default(""),
|
enMonsterSpecialDesc: z.string().default(""),
|
||||||
enMonsterSpecialMechanicName: z.array(z.string()),
|
enMonsterSpecialMechanicName: z.array(z.string()),
|
||||||
enLevelMaxDescription: z.array(z.string()),
|
enLevelMaxDescription: z.array(z.string()),
|
||||||
|
enRecommendedLevelMechanic: z.array(z.string()).default([]),
|
||||||
|
tagType: z.array(z.enum(["Advantage", "Disadvantage", "None"])).default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const lunarisStygianScheduleSchema = z.object({
|
export const lunarisStygianScheduleSchema = z.object({
|
||||||
@@ -88,6 +90,8 @@ export function selectedStygianBosses(value: z.infer<typeof lunarisStygianSchedu
|
|||||||
hp: boss.monsterStats.hp,
|
hp: boss.monsterStats.hp,
|
||||||
description: boss.enMonsterSpecialDesc,
|
description: boss.enMonsterSpecialDesc,
|
||||||
imageName: stygianImageName(boss.specialMonsterIcon),
|
imageName: stygianImageName(boss.specialMonsterIcon),
|
||||||
|
recommendedMechanics: boss.enRecommendedLevelMechanic.map((entry) => entry.trim()),
|
||||||
|
recommendedMechanicTypes: boss.tagType,
|
||||||
mechanics: boss.enMonsterSpecialMechanicName.flatMap((name, index) => {
|
mechanics: boss.enMonsterSpecialMechanicName.flatMap((name, index) => {
|
||||||
const description = boss.enLevelMaxDescription[index]?.trim();
|
const description = boss.enLevelMaxDescription[index]?.trim();
|
||||||
return name.trim() && description ? [{ name: name.trim(), description }] : [];
|
return name.trim() && description ? [{ name: name.trim(), description }] : [];
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const nextConfig: NextConfig = {
|
|||||||
qualities: [75, 100],
|
qualities: [75, 100],
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{ protocol: "https", hostname: "buzz-cdn.astrxl.dev", pathname: "/**" },
|
{ protocol: "https", hostname: "buzz-cdn.astrxl.dev", pathname: "/**" },
|
||||||
|
{ protocol: "https", hostname: "api.lunaris.moe", pathname: "/data/assets/icons/**" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Reference in New Issue
Block a user