Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions data/enrichment/blood-hunts-map.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions data/enrichment/coverage-thresholds.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
"minCoveragePct": 0.4,
"warnCoveragePct": 0.9
},
"npc-portrait-map": {
"minMatchedCount": 0,
"minCoveragePct": 0,
"warnCoveragePct": 0.5
},
"workstation-display-map": {
"minMatchedCount": 120,
"minCoveragePct": 0.8,
Expand Down
7 changes: 7 additions & 0 deletions data/enrichment/enrichment-coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@
"signal": "high-signal",
"lowSignalExcluded": 596
},
"npc-portrait-map": {
"total": 69,
"matched": 50,
"coveragePct": 0.7246,
"signal": "high-signal",
"lowSignalExcluded": 0
},
"quest-display-map": {
"total": 163,
"matched": 0,
Expand Down
5,405 changes: 5,405 additions & 0 deletions data/enrichment/npc-portrait-candidates.json

Large diffs are not rendered by default.

840 changes: 840 additions & 0 deletions data/enrichment/npc-portrait-map.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"build": "npm run generate:data && npm run validate:data && vite build && jiti scripts/prepare-pages-fallback.ts",
"preview": "vite preview",
"verify": "npx tsc --noEmit && node scripts/check_shortcode_syntax.js content && npm run build && npm run verify:artifact",
"test": "npm run test:visual-review && npm run test:ability-damage-evidence && tsx --test src/components/layout/SiteShell.test.tsx",
"test": "npm run test:visual-review && npm run test:ability-damage-evidence && tsx scripts/blood-hunts.test.ts && tsx scripts/npc-portraits.test.ts && tsx --test src/components/layout/SiteShell.test.tsx",
"visual:baseline": "npm run build && npm run visual:baseline:capture",
"visual:baseline:capture": "jiti scripts/visual-review.ts baseline",
"visual:compare": "npm run build && npm run visual:compare:capture",
Expand Down
29 changes: 27 additions & 2 deletions scripts/blood-hunts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import assert from "node:assert/strict";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { buildBloodHuntsMapSnapshot } from "./blood-hunts";
import { buildBloodHuntsMapSnapshot, nameKeyToLocalizationGuid } from "./blood-hunts";

type TestCase = {
name: string;
Expand Down Expand Up @@ -34,7 +34,7 @@ function validSource(overrides: Record<string, unknown> = {}) {
Level: 53,
HideLevel: 1,
PrefabGUID: { _Value: 795262842 },
Name: { Key: { _a: 1, _b: 2, _c: 3, _d: 4 } },
Name: { Key: { _a: 1819955650, _b: -1253071585, _c: 362143916, _d: 236122505 } },
AssetGuid: "must-not-be-promoted",
SpritePathID: 987
}
Expand All @@ -49,14 +49,23 @@ async function buildFixture(sourceFile: string, overrides: Partial<Parameters<ty
sourceRef: "MonoBehaviour/BloodHuntsDataAuthoring.json",
prefabByGuid: new Map([[795262842, "CHAR_Vampire_IceRanger_VBlood"]]),
localizedNamesByGuid: { "795262842": "General Elena the Hollow" },
localizedTextByGuid: { "c2517a6c-1fa5-4fb5-ace0-951589f1120e": "General Elena the Hollow" },
npcDisplayByPrefab: { CHAR_Vampire_IceRanger_VBlood: { displayNameEn: "General Elena the Hollow" } },
prefabSourceRef: "data/prefabs/All.json",
localizedNameSourceRef: "data/enrichment/prefab-localization.json:namesByGuid",
localizedTextSourceRef: "Bloodcraft/Resources/Localization/English.json:Nodes",
npcDisplaySourceRef: "data/enrichment/npc-display-map.json",
...overrides
});
}

test("nameKeyToLocalizationGuid converts Unity localization key chunks to canonical GUID text", () => {
assert.equal(
nameKeyToLocalizationGuid({ _a: 1819955650, _b: -1253071585, _c: 362143916, _d: 236122505 }),
"c2517a6c-1fa5-4fb5-ace0-951589f1120e"
);
});

test("buildBloodHuntsMapSnapshot keys rows only by PrefabGUID._Value", async () => {
await withSourceFile(validSource(), async (sourceFile) => {
const snapshot = await buildFixture(sourceFile);
Expand All @@ -75,6 +84,14 @@ test("buildBloodHuntsMapSnapshot converts HideLevel to boolean and records sourc
});
});

test("buildBloodHuntsMapSnapshot stores source-backed nameLocalizationGuid", async () => {
await withSourceFile(validSource(), async (sourceFile) => {
const snapshot = await buildFixture(sourceFile);
const entry = snapshot.entriesByGuid["795262842"];
assert.equal(entry.nameLocalizationGuid, "c2517a6c-1fa5-4fb5-ace0-951589f1120e");
});
});

test("buildBloodHuntsMapSnapshot requires source-backed prefab, localization, and NPC display joins", async () => {
await withSourceFile(validSource(), async (sourceFile) => {
await assert.rejects(
Expand All @@ -85,6 +102,14 @@ test("buildBloodHuntsMapSnapshot requires source-backed prefab, localization, an
() => buildFixture(sourceFile, { localizedNamesByGuid: {} }),
/missing localized name join/
);
await assert.rejects(
() => buildFixture(sourceFile, { localizedTextByGuid: {} }),
/missing localized text join/
);
await assert.rejects(
() => buildFixture(sourceFile, { localizedTextByGuid: { "c2517a6c-1fa5-4fb5-ace0-951589f1120e": "Wrong Name" } }),
/localized text mismatch/
);
await assert.rejects(
() => buildFixture(sourceFile, { npcDisplayByPrefab: {} }),
/missing NPC display join/
Expand Down
32 changes: 31 additions & 1 deletion scripts/blood-hunts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface BloodHuntsJoinProvenance {
sourceRef: string;
prefabSourceRef: string;
localizedNameSourceRef: string;
localizedTextSourceRef: string;
npcDisplaySourceRef: string;
hideLevelSourceValue: number;
}
Expand All @@ -24,6 +25,7 @@ export interface BloodHuntsMapEntry {
bloodHuntLevel: number;
bloodHuntHideLevel: boolean;
nameKey: BloodHuntsNameKey;
nameLocalizationGuid: string;
provenance: BloodHuntsJoinProvenance;
}

Expand All @@ -40,9 +42,11 @@ export interface BloodHuntsBuildOptions {
sourceRef: string;
prefabByGuid: Map<number, string>;
localizedNamesByGuid: Record<string, string>;
localizedTextByGuid: Record<string, string> | Map<string, string>;
npcDisplayByPrefab: Record<string, { displayNameEn?: string } | undefined>;
prefabSourceRef: string;
localizedNameSourceRef: string;
localizedTextSourceRef: string;
npcDisplaySourceRef: string;
}

Expand Down Expand Up @@ -72,6 +76,20 @@ function readNameKey(value: unknown, source: string): BloodHuntsNameKey {
};
}

export function nameKeyToLocalizationGuid(key: BloodHuntsNameKey): string {
const bytes = Buffer.alloc(16);
bytes.writeInt32LE(key._a, 0);
bytes.writeInt32LE(key._b, 4);
bytes.writeInt32LE(key._c, 8);
bytes.writeInt32LE(key._d, 12);
const hex = bytes.toString("hex");
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}

function readLocalizedText(localizedTextByGuid: BloodHuntsBuildOptions["localizedTextByGuid"], guid: string): string | undefined {
return localizedTextByGuid instanceof Map ? localizedTextByGuid.get(guid.toLowerCase()) : localizedTextByGuid[guid.toLowerCase()];
}

function parseBloodHuntsRows(raw: unknown, sourceRef: string): BloodHuntsMapEntry[] {
if (!isRecord(raw) || !Array.isArray(raw.VBloodDatas)) {
throw new Error(`${sourceRef}: expected VBloodDatas array`);
Expand All @@ -87,17 +105,20 @@ function parseBloodHuntsRows(raw: unknown, sourceRef: string): BloodHuntsMapEntr
throw new Error(`${source}: missing PrefabGUID._Value`);
}
const hideLevelSourceValue = readNumber(row, "HideLevel", source);
const nameKey = readNameKey(isRecord(row.Name) ? row.Name.Key : undefined, source);
return {
prefab: "",
guid: prefabGuid,
bloodHuntLevel: readNumber(row, "Level", source),
bloodHuntHideLevel: hideLevelSourceValue !== 0,
nameKey: readNameKey(isRecord(row.Name) ? row.Name.Key : undefined, source),
nameKey,
nameLocalizationGuid: nameKeyToLocalizationGuid(nameKey),
provenance: {
sourceKind: bloodHuntsSourceKind,
sourceRef,
prefabSourceRef: "",
localizedNameSourceRef: "",
localizedTextSourceRef: "",
npcDisplaySourceRef: "",
hideLevelSourceValue
}
Expand Down Expand Up @@ -126,6 +147,14 @@ export async function buildBloodHuntsMapSnapshot(options: BloodHuntsBuildOptions
throw new Error(`${options.sourceRef}:${guidKey}: missing localized name join in ${options.localizedNameSourceRef}`);
}

const localizedText = readLocalizedText(options.localizedTextByGuid, row.nameLocalizationGuid)?.trim();
if (!localizedText) {
throw new Error(`${options.sourceRef}:${guidKey}: missing localized text join for ${row.nameLocalizationGuid} in ${options.localizedTextSourceRef}`);
}
if (localizedText !== localizedName) {
throw new Error(`${options.sourceRef}:${guidKey}: localized text mismatch for ${row.nameLocalizationGuid} between ${options.localizedTextSourceRef} and ${options.localizedNameSourceRef}`);
}

const displayName = options.npcDisplayByPrefab[prefab]?.displayNameEn?.trim();
if (!displayName) {
throw new Error(`${options.sourceRef}:${guidKey}: missing NPC display join in ${options.npcDisplaySourceRef}`);
Expand All @@ -138,6 +167,7 @@ export async function buildBloodHuntsMapSnapshot(options: BloodHuntsBuildOptions
...row.provenance,
prefabSourceRef: options.prefabSourceRef,
localizedNameSourceRef: options.localizedNameSourceRef,
localizedTextSourceRef: options.localizedTextSourceRef,
npcDisplaySourceRef: options.npcDisplaySourceRef
}
});
Expand Down
Loading
Loading