import { supportsWeaponType } from "@/lib/catalog/lunaris"; import * as z from "zod"; export interface TeamToolMember { characterKey: string; weaponKey: string | null; artifactKey: string | null; secondArtifactKey: string | null; constellation: number; refinement: number; damage: string; } export interface TeamToolValue { clientId: string; title: string; rotationSeconds: string; combo: string; members: TeamToolMember[]; } export const teamApplyFields = ["loadout", "damage", "rotation", "combo"] as const; export type TeamApplyField = (typeof teamApplyFields)[number]; export const teamMemberApplyFields = [ "characterKey", "weaponKey", "artifactKey", "secondArtifactKey", "constellation", "refinement", "damage", ] as const; export type TeamMemberApplyField = (typeof teamMemberApplyFields)[number]; export type TeamApplySelection = | TeamApplyField | `member:${number}:${TeamMemberApplyField}`; export interface TeamPreset { id: string; name: string; createdAt: string; team: Omit; } const transferMemberSchema = z.strictObject({ characterKey: z.string().trim().min(1).max(64), weaponKey: z.string().trim().min(1).max(64).nullable(), artifactKey: z.string().trim().min(1).max(64).nullable(), secondArtifactKey: z.string().trim().min(1).max(64).nullable(), constellation: z.number().int().min(0).max(6), refinement: z.number().int().min(1).max(5), damage: z.string().refine((value) => { const number = Number(value); return Number.isFinite(number) && number >= 0 && number <= 999_999_999_999; }), }); const transferTeamSchema = z.strictObject({ title: z.string().trim().min(1).max(160), rotationSeconds: z.string().refine((value) => { const number = Number(value); return Number.isFinite(number) && number > 0 && number <= 9999; }), combo: z.string().trim().min(1).max(2_000), members: z.array(transferMemberSchema).length(4) .refine( (members) => new Set(members.map((member) => member.characterKey)).size === 4, ) .refine((members) => members.some((member) => Number(member.damage) > 0)), }); const teamTransferSchema = z.strictObject({ type: z.literal("buzz-sheet.team"), version: z.literal(1), team: transferTeamSchema, }); export function serializeTeamTransfer(team: TeamToolValue): string { const payload = teamTransferSchema.parse({ type: "buzz-sheet.team", version: 1, team: { title: team.title, rotationSeconds: team.rotationSeconds, combo: team.combo, members: team.members, }, }); return JSON.stringify(payload, null, 2); } export function teamTransferFileName(teamName: string): string { const safeName = teamName .replace(/[<>:"/\\|?*\u0000-\u001f]/gu, "-") .replace(/[. ]+$/gu, "") .trim(); return `${safeName || "team"}.json`; } export function parseTeamTransfer(serialized: string): Omit { const payload = teamTransferSchema.parse(JSON.parse(serialized) as unknown); return { ...payload.team, members: payload.team.members.map((member) => ({ ...member })), }; } export function cloneTeamImports( teams: readonly Omit[], createId: () => string, ): TeamToolValue[] { return teams.map((team) => ({ clientId: createId(), ...team, members: team.members.map((member) => ({ ...member })), })); } export function findInvalidTeamCatalogReference( team: Omit, characters: readonly TeamToolCatalogItem[], weapons: readonly TeamToolCatalogItem[], artifacts: readonly TeamToolCatalogItem[], ): string | null { const characterByKey = new Map(characters.map((item) => [item.key, item])); const weaponByKey = new Map(weapons.map((item) => [item.key, item])); const artifactByKey = new Map(artifacts.map((item) => [item.key, item])); for (const member of team.members) { const character = characterByKey.get(member.characterKey); if (!character) return `ไม่พบตัวละคร ${member.characterKey} ใน Catalog`; const weapon = member.weaponKey ? weaponByKey.get(member.weaponKey) : null; if (member.weaponKey && !weapon) return `ไม่พบอาวุธ ${member.weaponKey} ใน Catalog`; if (weapon && !supportsWeaponType(character.weaponType, weapon.weaponType)) { return `${character.name} ไม่รองรับอาวุธ ${weapon.name}`; } for (const artifactKey of [member.artifactKey, member.secondArtifactKey]) { if (artifactKey && !artifactByKey.has(artifactKey)) { return `ไม่พบ Artifact ${artifactKey} ใน Catalog`; } } } return null; } interface TeamToolCatalogItem { key: string; name: string; weaponType?: string | null; } export function findUnsupportedTeamWeapon( teams: readonly TeamToolValue[], teamIds: ReadonlySet, characters: readonly TeamToolCatalogItem[], weapons: readonly TeamToolCatalogItem[], ): { characterName: string; weaponName: string } | null { const characterByKey = new Map(characters.map((item) => [item.key, item])); const weaponByKey = new Map(weapons.map((item) => [item.key, item])); for (const team of teams) { if (!teamIds.has(team.clientId)) continue; for (const member of team.members) { if (!member.characterKey || !member.weaponKey) continue; const character = characterByKey.get(member.characterKey); const weapon = weaponByKey.get(member.weaponKey); if ( character && weapon && !supportsWeaponType(character.weaponType, weapon.weaponType) ) { return { characterName: character.name, weaponName: weapon.name }; } } } return null; } function cloneMember(member: TeamToolMember): TeamToolMember { return { ...member }; } function cloneTeam(team: TeamToolValue): TeamToolValue { return { ...team, members: team.members.map(cloneMember) }; } export function applyTeamFields( teams: readonly TeamToolValue[], sourceId: string, targetIds: ReadonlySet, fields: readonly TeamApplySelection[], ): TeamToolValue[] { const source = teams.find((team) => team.clientId === sourceId); if (!source) return teams.map(cloneTeam); const selected = new Set(fields); return teams.map((team) => { if (!targetIds.has(team.clientId) || team.clientId === sourceId) return cloneTeam(team); const next = cloneTeam(team); if (selected.has("loadout")) { next.members = source.members.map((member, index) => ({ ...cloneMember(member), damage: next.members[index]?.damage ?? "0", })); } if (selected.has("damage")) next.members = source.members.map(cloneMember); for (const field of fields) { const match = /^member:(\d+):(.+)$/.exec(field); if (!match) continue; const memberIndex = Number(match[1]); const memberField = match[2] as TeamMemberApplyField; const sourceMember = source.members[memberIndex]; const matchingMemberIndex = sourceMember?.characterKey ? next.members.findIndex( (member) => member.characterKey === sourceMember.characterKey, ) : -1; const targetMemberIndex = matchingMemberIndex === -1 ? memberIndex : matchingMemberIndex; const targetMember = next.members[targetMemberIndex]; if (!sourceMember || !targetMember || !teamMemberApplyFields.includes(memberField)) continue; next.members[targetMemberIndex] = { ...targetMember, [memberField]: sourceMember[memberField], }; } if (selected.has("rotation")) next.rotationSeconds = source.rotationSeconds; if (selected.has("combo")) next.combo = source.combo; return next; }); } export function applyPresetFields( teams: readonly TeamToolValue[], preset: TeamPreset, targetIds: ReadonlySet, fields: readonly TeamApplySelection[], ): TeamToolValue[] { return applyTeamFields( [ ...teams, { clientId: preset.id, ...preset.team, members: preset.team.members.map(cloneMember) }, ], preset.id, targetIds, fields, ).slice(0, teams.length); } function presetFingerprint(team: Omit): string { return JSON.stringify(team); } export function limitPresets(presets: readonly TeamPreset[], limit = 5): TeamPreset[] { const seen = new Set(); return presets.filter((preset) => { const fingerprint = presetFingerprint(preset.team); if (seen.has(fingerprint)) return false; seen.add(fingerprint); return true; }).slice(0, limit); } export function parseTeamPresets(serialized: string | null): TeamPreset[] { if (!serialized) return []; try { const value: unknown = JSON.parse(serialized); if (!Array.isArray(value)) return []; return limitPresets(value.filter((item): item is TeamPreset => { if (!item || typeof item !== "object") return false; const candidate = item as Record; return typeof candidate.id === "string" && typeof candidate.name === "string" && typeof candidate.createdAt === "string" && Boolean(candidate.team); })); } catch { return []; } } export function teamPresetStorageKey(guideId: string): string { return `buzz-sheet:team-presets:${guideId}`; } export function teamPresetCookieKey(guideId: string): string { return `buzz-sheet-team-presets-${guideId}`; }