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>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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";
|
||||
}
|
||||
Reference in New Issue
Block a user