feat : add import from buzz guide
CI / verify (push) Successful in 59s
CI / build-and-deploy (push) Successful in 1m38s

This commit is contained in:
2026-09-20 00:41:02 +07:00 Unverified
parent 3fe17a2ff7
commit b3bc4c3bb4
7 changed files with 1406 additions and 81 deletions
+61
View File
@@ -0,0 +1,61 @@
import "server-only";
import { and, asc, eq, isNull } from "drizzle-orm";
import { connection } from "next/server";
import { getDb } from "@/db/client";
import { guides, teamCompositions, teamConditions, teamMembers } from "@/db/schema";
import type { TeamGuide } from "@/lib/team-guides";
export async function getTeamGuides(): Promise<TeamGuide[]> {
await connection();
if (!process.env.DATABASE_URL) return [];
try {
const rows = await getDb().select({
guideId: guides.id,
guideCharacterId: guides.characterKey,
guideName: guides.name,
teamId: teamCompositions.id,
teamTitle: teamCompositions.title,
combo: teamCompositions.combo,
conditionName: teamConditions.name,
position: teamMembers.position,
characterId: teamMembers.characterKey,
weaponId: teamMembers.weaponKey,
artifactOneId: teamMembers.artifactKey,
artifactTwoId: teamMembers.secondArtifactKey,
constellation: teamMembers.constellation,
refinement: teamMembers.refinement,
}).from(guides)
.innerJoin(teamCompositions, eq(teamCompositions.guideId, guides.id))
.innerJoin(teamMembers, eq(teamMembers.teamId, teamCompositions.id))
.leftJoin(teamConditions, eq(teamConditions.id, teamCompositions.conditionId))
.where(and(eq(guides.isPublic, true), isNull(guides.trashedAt)))
.orderBy(asc(guides.sortOrder), asc(guides.name), asc(teamConditions.sortOrder), asc(teamCompositions.sortOrder), asc(teamMembers.position));
const result: TeamGuide[] = [];
for (const row of rows) {
let guide = result.at(-1);
if (!guide || guide.id !== row.guideId) {
guide = { id: row.guideId, characterId: row.guideCharacterId, name: row.guideName, teams: [] };
result.push(guide);
}
let team = guide.teams.at(-1);
if (!team || team.id !== row.teamId) {
team = { id: row.teamId, title: row.teamTitle, conditionName: row.conditionName, combo: row.combo, members: [] };
guide.teams.push(team);
}
team.members.push({
position: row.position,
characterId: row.characterId,
weaponId: row.weaponId,
artifactOneId: row.artifactOneId,
artifactTwoId: row.artifactTwoId,
constellation: row.constellation,
refinement: row.refinement,
});
}
return result;
} catch (error) {
console.error("Team guides unavailable", error);
return [];
}
}
+50
View File
@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import { generateTeam } from "./translator/generate";
import type { Catalog, CatalogItem } from "./translator/types";
import { importGuideTeam, type TeamGuideTeam } from "./team-guides";
const item = (id: string, key: string, name: string, weaponType?: string): CatalogItem => ({ id, key, name, imageUrl: "", rarity: 5, weaponType, supported: true, baseStats: [] });
const catalog: Catalog = {
version: "test",
characters: [item("char-1", "mavuika", "Mavuika", "claymore"), item("char-2", "xilonen", "Xilonen", "sword")],
weapons: [item("weapon-1", "favoniusgreatsword", "Favonius Greatsword", "claymore"), item("weapon-2", "favoniussword", "Favonius Sword", "sword")],
artifacts: [item("set-1", "obsidiancodex", "Obsidian Codex"), item("set-2", "scrolloftheheroofcindercity", "Scroll")],
};
const guideTeam = (combo = "Mavuika E > Xilonen E N2"): TeamGuideTeam => ({
id: "team-1",
title: "Guide team",
conditionName: "Main Sheet",
combo,
members: [
{ position: 0, characterId: "char-1", weaponId: "weapon-1", artifactOneId: "set-1", artifactTwoId: null, constellation: 2, refinement: 1 },
{ position: 1, characterId: "char-2", weaponId: "weapon-2", artifactOneId: "set-1", artifactTwoId: "set-2", constellation: 0, refinement: 5 },
],
});
describe("team guide import", () => {
it("maps guide loadouts and defaults into a generated team", () => {
const result = importGuideTeam(guideTeam(), catalog);
expect(result.error).toBeUndefined();
expect(result.team!.builds[0]).toMatchObject({ characterKey: "mavuika", weaponKey: "favoniusgreatsword", constellation: 2, refinement: 1, artifactMode: "4pc", artifactOneKey: "obsidiancodex", level: 90, talents: [9, 9, 9], sands: "atk%", goblet: "atk%", circlet: "cr" });
expect(result.team!.builds[1]).toMatchObject({ artifactMode: "2+2", artifactOneKey: "obsidiancodex", artifactTwoKey: "scrolloftheheroofcindercity", refinement: 5 });
expect(result.team!.rotation).toMatchObject({ enabled: true, text: "Mavuika E > Xilonen E N2", repeat: false });
expect(generateTeam(result.team!)).toContain('xilonen add set="scrolloftheheroofcindercity" count=2;');
});
it("preserves unsupported combo text with rotation disabled", () => {
const result = importGuideTeam(guideTeam("Mavuika 3[C ED] Q"), catalog);
expect(result.team!.rotation).toEqual({ enabled: false, text: "Mavuika 3[C ED] Q", repeat: false });
expect(generateTeam(result.team!)).not.toContain("# Rotation");
});
it.each(["", " - "])("omits an empty guide combo %#", (combo) => {
expect(importGuideTeam(guideTeam(combo), catalog).team!.rotation).toBeUndefined();
});
it("reports missing catalog entries before importing", () => {
const source = guideTeam();
source.members[0].weaponId = "missing";
expect(importGuideTeam(source, catalog).error).toBe("Weapon missing is missing from the catalog.");
});
});
+71
View File
@@ -0,0 +1,71 @@
import { createBuild } from "./translator/build";
import { translateRotation } from "./translator/rotation";
import type { Catalog, Team } from "./translator/types";
export interface TeamGuideMember {
position: number;
characterId: string;
weaponId: string;
artifactOneId: string;
artifactTwoId: string | null;
constellation: number;
refinement: number;
}
export interface TeamGuideTeam {
id: string;
title: string;
conditionName: string | null;
combo: string;
members: TeamGuideMember[];
}
export interface TeamGuide {
id: string;
characterId: string;
name: string;
teams: TeamGuideTeam[];
}
export type GuideImportResult = { team: Team; error?: never } | { team?: never; error: string };
export function importGuideTeam(source: TeamGuideTeam, catalog: Catalog): GuideImportResult {
const builds = [] as Team["builds"];
for (const member of source.members) {
const character = catalog.characters.find((item) => item.id === member.characterId);
const weapon = catalog.weapons.find((item) => item.id === member.weaponId);
const artifactOne = catalog.artifacts.find((item) => item.id === member.artifactOneId);
const artifactTwo = member.artifactTwoId ? catalog.artifacts.find((item) => item.id === member.artifactTwoId) : undefined;
if (!character) return { error: `Character ${member.characterId} is missing from the catalog.` };
if (!weapon) return { error: `Weapon ${member.weaponId} is missing from the catalog.` };
if (!artifactOne) return { error: `Artifact ${member.artifactOneId} is missing from the catalog.` };
if (member.artifactTwoId && !artifactTwo) return { error: `Artifact ${member.artifactTwoId} is missing from the catalog.` };
if (character.weaponType && weapon.weaponType !== character.weaponType) return { error: `${weapon.name} is not compatible with ${character.name}.` };
const build = createBuild(catalog, character, `guide-build-${source.id}-${member.position}`);
builds.push({
...build,
weaponId: weapon.id,
weaponKey: weapon.key,
weaponName: weapon.name,
refinement: member.refinement,
constellation: member.constellation,
artifactMode: artifactTwo ? "2+2" : "4pc",
artifactOneId: artifactOne.id,
artifactOneKey: artifactOne.key,
artifactOneName: artifactOne.name,
artifactTwoId: artifactTwo?.id ?? "",
artifactTwoKey: artifactTwo?.key ?? "",
artifactTwoName: artifactTwo?.name ?? "",
});
}
if (!builds.length) return { error: "This guide team has no characters." };
const team: Team = { id: `guide-team-${source.id}`, name: source.title, builds };
const combo = source.combo.trim();
if (combo && combo !== "-") {
team.rotation = { enabled: true, text: combo, repeat: false };
if (translateRotation(team).error) team.rotation.enabled = false;
}
return { team };
}