Files
genshin-assistant/src/lib/storedArtifactAdapter.ts
T
AzuTear e76d88e0c7 chore: initialize repository baseline
Import the existing Electron + React + TypeScript app as the version-control
baseline before the scanner rework (C# input/capture sidecar, resolution-anchored
layout profiles, OCR preprocessing, eval harness, rescan-merge, GOOD interop).

Housekeeping in this commit:
- Remove orphaned temp_inputhelper_block.ts (duplicate of the input-helper script).
- Ignore .claude/scheduled_tasks.lock local session state.
- Add .gitattributes to normalize line endings (LF in repo).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:31:01 +02:00

140 lines
4.7 KiB
TypeScript

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<string, ArtifactSlot> = {
[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";
}