169 lines
5.5 KiB
JavaScript
169 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
import { readFile, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const sourceFile = path.join(projectRoot, "dungeon-pipeline", "epoch-five-player-instances.json");
|
|
const campaignFile = path.join(projectRoot, "dungeon-pipeline", "dungeon-campaign.json");
|
|
const recipesRoot = path.join(projectRoot, "dungeon-pipeline", "recipes");
|
|
const manastormRecipesRoot = path.join(
|
|
projectRoot,
|
|
"dungeon-pipeline",
|
|
"manastorm",
|
|
"recipes",
|
|
);
|
|
|
|
const stableValue = (value) => {
|
|
if (Array.isArray(value)) return value.map(stableValue);
|
|
if (!value || typeof value !== "object") return value;
|
|
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
|
|
};
|
|
const stableJson = (value) => `${JSON.stringify(stableValue(value), null, 2)}\n`;
|
|
const readJson = async (file) => JSON.parse(await readFile(file, "utf8"));
|
|
const slugPart = (value) => String(value ?? "")
|
|
.normalize("NFKD")
|
|
.replace(/[\u0300-\u036f]/g, "")
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-|-$/g, "") || "unknown";
|
|
|
|
const source = await readJson(sourceFile);
|
|
const campaign = await readJson(campaignFile);
|
|
if (source.schemaVersion !== 1 || !Array.isArray(source.dungeons)) {
|
|
throw new Error("Unsupported Epoch dungeon source manifest.");
|
|
}
|
|
|
|
const mapIds = new Set();
|
|
const slugs = new Set();
|
|
for (const dungeon of source.dungeons) {
|
|
if (!Number.isInteger(dungeon.mapId) || mapIds.has(dungeon.mapId)) {
|
|
throw new Error(`Duplicate or invalid Epoch map ID ${dungeon.mapId}.`);
|
|
}
|
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(dungeon.slug) || slugs.has(dungeon.slug)) {
|
|
throw new Error(`Duplicate or invalid Epoch slug ${dungeon.slug}.`);
|
|
}
|
|
mapIds.add(dungeon.mapId);
|
|
slugs.add(dungeon.slug);
|
|
const recipe = {
|
|
schemaVersion: 1,
|
|
slug: dungeon.slug,
|
|
title: dungeon.title,
|
|
mapId: dungeon.mapId,
|
|
lfgDungeonIds: dungeon.lfgDungeonIds,
|
|
expansion: dungeon.expansion,
|
|
clientBuild: source.source.clientBuild,
|
|
mapDirectory: `World/Maps/${dungeon.mapDirectory}`,
|
|
environmentMode: "detect",
|
|
difficultyVariants: [
|
|
{
|
|
id: "normal",
|
|
difficulty: "normal",
|
|
mapDifficultyId: 0,
|
|
spawnMask: 1,
|
|
levelRange: dungeon.normalLevelRange,
|
|
},
|
|
{
|
|
id: "heroic",
|
|
difficulty: "heroic",
|
|
mapDifficultyId: 1,
|
|
spawnMask: 2,
|
|
levelRange: dungeon.heroicLevelRange,
|
|
},
|
|
],
|
|
clientSource: {
|
|
rootEnvironmentVariable: source.source.rootEnvironmentVariable,
|
|
relativeDefault: "../epoch_live",
|
|
},
|
|
serverSource: {
|
|
rootEnvironmentVariable: "WOW_SERVER_DATA",
|
|
adapter: "normalized-json",
|
|
readOnly: true,
|
|
},
|
|
extraction: { adtTiles: "all-present" },
|
|
navigation: {
|
|
cellSize: 0.3,
|
|
cellHeight: 0.15,
|
|
tileSize: 256,
|
|
retainTileIntermediates: false,
|
|
anchorComponentMaximumHorizontalSpan: 1600,
|
|
agentHeight: 1.8,
|
|
agentRadius: 0.36,
|
|
agentMaxClimb: 0.6,
|
|
agentMaxSlope: 50,
|
|
},
|
|
review: {
|
|
entrance: "required",
|
|
bossOrder: "required",
|
|
optionalEncounters: "required",
|
|
presentation: "required",
|
|
offMeshLinks: "required",
|
|
},
|
|
exceptions: [],
|
|
warnings: [
|
|
"Environment geometry is sourced from the installed Project Epoch 3.3.5a client.",
|
|
"Population, creature spells, patrols, and encounter credit identities are sourced from the pinned AzerothCore world snapshot.",
|
|
],
|
|
};
|
|
await writeFile(path.join(recipesRoot, `${dungeon.slug}.json`), stableJson(recipe), "utf8");
|
|
const manastormSlug = `${dungeon.mapId}-${slugPart(dungeon.mapDirectory)}`;
|
|
await writeFile(
|
|
path.join(manastormRecipesRoot, `${manastormSlug}.json`),
|
|
stableJson({
|
|
schemaVersion: 1,
|
|
id: `manastorm:asset-recipe:${dungeon.mapId}`,
|
|
slug: manastormSlug,
|
|
mapId: dungeon.mapId,
|
|
location: dungeon.title,
|
|
mapDirectory: dungeon.mapDirectory,
|
|
encounterIds: [],
|
|
sourceCandidates: [],
|
|
clientSnapshot: "project-epoch-installed-client",
|
|
shipping: {
|
|
visual: `${manastormSlug}/visual.glb`,
|
|
collision: `${manastormSlug}/collision.glb`,
|
|
navigation: `${manastormSlug}/navigation.glb`,
|
|
},
|
|
transform: {
|
|
position: [0, 0, 0],
|
|
rotation: [0, 0, 0],
|
|
scale: 1,
|
|
fromWow: "three(x,y,z)=(-wow.x,wow.z,wow.y)",
|
|
},
|
|
review: {
|
|
sourceSelection: "selected",
|
|
playerAnchor: "required",
|
|
trashAnchors: "required",
|
|
bossAnchor: "required",
|
|
portalAnchor: "required",
|
|
resurrectionAnchor: "required",
|
|
navmeshProjection: "required",
|
|
},
|
|
}),
|
|
"utf8",
|
|
);
|
|
}
|
|
|
|
const retained = campaign.dungeons.filter((entry) => !mapIds.has(entry.mapId));
|
|
const insertionIndex = retained.findIndex((entry) => entry.source === "ascension");
|
|
const additions = source.dungeons.map((dungeon) => ({
|
|
slug: dungeon.slug,
|
|
mapId: dungeon.mapId,
|
|
source: "epoch",
|
|
}));
|
|
campaign.dungeons.splice(
|
|
0,
|
|
campaign.dungeons.length,
|
|
...(insertionIndex < 0
|
|
? [...retained, ...additions]
|
|
: [...retained.slice(0, insertionIndex), ...additions, ...retained.slice(insertionIndex)]),
|
|
);
|
|
await writeFile(campaignFile, stableJson(campaign), "utf8");
|
|
|
|
console.log(JSON.stringify({
|
|
status: "complete",
|
|
campaignDungeons: campaign.dungeons.length,
|
|
epochDungeons: additions.length,
|
|
recipes: additions.map((entry) => `dungeon-pipeline/recipes/${entry.slug}.json`),
|
|
}, null, 2));
|