import presetsJson from "../../data/presets.json"; import type { StoredArtifactRecord } from "../types/storage"; import type { Artifact, ArtifactSlot, ScanSource, ArtifactSubstat } from "../types/domain"; const setNameToKey = new Map( Object.entries(presetsJson.sets).map(([key, name]) => [simplify(String(name)), key]), ); const slotMap: Record = { [simplify("Flower of Life")]: "flower", [simplify("Plume of Death")]: "plume", [simplify("Sands of Eon")]: "sands", [simplify("Goblet of Eonothem")]: "goblet", [simplify("Circlet of Logos")]: "circlet", }; const statNames = [ "CRIT Rate", "CRIT DMG", "Energy Recharge", "Elemental Mastery", "Physical DMG Bonus", "Hydro DMG Bonus", "Pyro DMG Bonus", "Electro DMG Bonus", "Cryo DMG Bonus", "Dendro DMG Bonus", "Anemo DMG Bonus", "Geo DMG Bonus", "Healing Bonus", "ATK", "HP", "DEF", "ATK%", "HP%", "DEF%", ]; export function storedArtifactsToDomain(records: StoredArtifactRecord[]): Artifact[] { const now = new Date().toISOString(); return records .map((record): Artifact | null => { const slot = toSlot(record.slot); if (!slot) return null; return { id: record.id, setKey: toSetKey(record.setName), setName: record.setName || "Unknown set", slot, rarity: 5, level: typeof record.level === "number" ? record.level : inferLevel(record), mainStat: normalizeMainStat(record.mainStat, record.mainValue), substats: record.substats.map(parseStoredSubstat).filter(Boolean) as ArtifactSubstat[], equipped: isUsefulEquippedName(record.equipped) ? record.equipped.trim() : undefined, locked: !record.needsReview && record.confidence >= 90, source: toSource(record.source), confidence: Math.max(0, Math.min(1, record.confidence / 100)), lastSeenAt: record.lastSeenAt ?? record.firstSeenAt ?? now, }; }) .filter(Boolean) as Artifact[]; } function isUsefulEquippedName(value: string) { return Boolean(value?.trim() && !/unknown|missing|not detected/i.test(value)); } function toSlot(value: string): ArtifactSlot | null { return slotMap[simplify(value)] ?? null; } function toSetKey(value: string) { const simplified = simplify(value); return setNameToKey.get(simplified) ?? slug(value || "unknown_set"); } function normalizeMainStat(stat: string, value: string) { const cleanStat = stat.replace(/\s+/g, " ").trim(); const cleanValue = value.trim(); if (/^(ATK|HP|DEF)$/i.test(cleanStat) && cleanValue.includes("%")) return `${cleanStat.toUpperCase()}%`; return canonicalStatName(cleanStat, cleanValue) || cleanStat || "Unknown main stat"; } function parseStoredSubstat(raw: string): ArtifactSubstat | null { const text = raw.replace(/[•+]/g, " ").replace(/\s+/g, " ").trim(); if (!text) return null; const stat = statNames.find((name) => simplify(text).includes(simplify(name.replace("%", "")))); const valueMatch = /([0-9]+(?:\.[0-9])?)\s*%?/.exec(text); if (!stat || !valueMatch) return null; const value = Number(valueMatch[1]); if (!Number.isFinite(value)) return null; const unit: ArtifactSubstat["unit"] = text.includes("%") ? "%" : "flat"; return { key: canonicalStatName(stat, unit === "%" ? `${value}%` : `${value}`) || stat, value, unit, }; } function canonicalStatName(stat: string, value: string) { const simple = simplify(stat); if (simple === "crit rate") return "CRIT Rate"; if (simple === "crit dmg" || simple === "crit damage") return "CRIT DMG"; if (simple === "energy recharge") return "Energy Recharge"; if (simple === "elemental mastery") return "Elemental Mastery"; if (simple === "atk" && value.includes("%")) return "ATK%"; if (simple === "hp" && value.includes("%")) return "HP%"; if (simple === "def" && value.includes("%")) return "DEF%"; if (simple === "atk") return "ATK"; if (simple === "hp") return "HP"; if (simple === "def") return "DEF"; return statNames.find((name) => simplify(name) === simple) ?? ""; } function inferLevel(record: StoredArtifactRecord) { // Backward compatibility for older OCR records written before level // persistence landed in the local store. return record.source === "manual-scan" || record.source === "auto-scan" ? 20 : 0; } function toSource(value: string): ScanSource { if (value === "manual-scan") return "manual"; if (value === "overlay") return "overlay"; if (value === "good_import") return "good_import"; return "screen"; } function simplify(value: string) { return value.toLowerCase().replace(/[^a-z0-9%]+/g, " ").trim(); } function slug(value: string) { return simplify(value).replace(/%/g, "percent").replace(/\s+/g, "_") || "unknown_set"; }