wynntils-waypoints/main.ts
2024-12-28 22:05:19 +01:00

223 lines
5.6 KiB
TypeScript

type MapLocation = {
x: number;
y: number;
z: number;
clusterId?: number;
};
type NPCLocation = {
icon: string | null;
description: string | null;
label: string;
name: string;
location: MapLocation;
};
type ProfessionNodeLocation =
& {
sourceMaterial: {
name: string;
level: number;
};
label: string;
name: string;
location: MapLocation;
}
& ({
materialType: "ore";
professionType: "mining";
} | {
materialType: "crop";
professionType: "farming";
} | {
materialType: "log";
professionType: "woodcutting";
} | {
materialType: "fish";
professionType: "fishing";
});
type ProfessionCraftingStationLocation = {
name: string;
label: string;
professionType:
| "alchemism"
| "armouring"
| "cooking"
| "jeweling"
| "scribing"
| "tailoring"
| "weaponsmithing"
| "woodworking";
location: MapLocation;
};
function getDistance(locationA: MapLocation, locationB: MapLocation): number {
return Math.sqrt(
(locationB.x - locationA.x) ** 2 + (locationB.y - locationA.y) ** 2 +
(locationB.z - locationA.z) ** 2,
);
}
function middle(...locations: MapLocation[]): MapLocation {
const sum = locations.reduce((prev, cur) => ({
x: prev.x + cur.x,
y: prev.y + cur.y,
z: prev.z + cur.z,
}));
return {
x: Math.round(sum.x / locations.length),
y: Math.round(sum.y / locations.length),
z: Math.round(sum.z / locations.length),
};
}
function groupLocationItems<T extends { location: MapLocation }>(
items: T[],
itemsAreSimilar: (valA: T, valB: T) => boolean,
getClusterName: (cluster: T[]) => string,
getClusterIcon: (cluster: T[]) => string,
) {
let clusterId = 0;
for (let rootIdx = 0; rootIdx < items.length; rootIdx++) {
const clusterRoot = items[rootIdx];
if (clusterRoot.location.clusterId === undefined) {
clusterRoot.location.clusterId = clusterId++;
}
for (let idx = rootIdx + 1; idx < items.length; idx++) {
if (
itemsAreSimilar(clusterRoot, items[idx])
) {
let originalClusterId = items[idx].location.clusterId;
if (
originalClusterId !== undefined &&
originalClusterId !== clusterRoot.location.clusterId
) {
for (
const itemWithOriginalClusterId of items.filter((item) =>
item.location.clusterId === originalClusterId
)
) {
itemWithOriginalClusterId.location.clusterId =
clusterRoot.location.clusterId;
}
} else {
items[idx].location.clusterId = clusterRoot.location.clusterId;
}
}
}
}
const clusteredItems: Record<number, T[]> = {};
for (const item of items) {
const groupByClusterid = item.location.clusterId;
if (groupByClusterid === undefined) {
throw new Error("clusterId should not be undefined here");
}
if (clusteredItems[groupByClusterid] === undefined) {
clusteredItems[groupByClusterid] = [];
}
clusteredItems[groupByClusterid].push(item);
}
const clusterPoints: Record<
number,
{
name: string;
icon: string;
color: "#ffffffff";
visibility: "default";
location: MapLocation;
}
> = {};
for (const [clusterId, cluster] of Object.entries(clusteredItems)) {
clusterPoints[clusterId] = {
name: getClusterName(cluster),
color: "#ffffffff",
icon: getClusterIcon(cluster),
visibility: "default",
location: middle(
...cluster.map((item) => item.location),
),
};
}
return clusterPoints;
}
function professionNodeLocationsAreSimilar(
nodeA: ProfessionNodeLocation,
nodeB: ProfessionNodeLocation,
): boolean {
if (
nodeA.materialType !== nodeB.materialType ||
nodeA.sourceMaterial.name !== nodeB.sourceMaterial.name
) return false;
const MAX_DISTANCE = nodeA.professionType === "woodcutting" ? 64 : 16;
return getDistance(nodeA.location, nodeB.location) <= MAX_DISTANCE;
}
function professionNodeLocationsClusterName(
cluster: ProfessionNodeLocation[],
): string {
if (
cluster.reduce(
(prev, cur) => prev === cur.name ? prev : undefined,
cluster[0].name as string | undefined,
) === undefined
) {
console.error(
"Can't find label",
cluster,
);
}
return cluster[0].name.replace("Node", "") + "x" + cluster.length;
}
function professionNodeLocationsClusterIcon(
cluster: ProfessionNodeLocation[],
): string {
if (
cluster.reduce(
(prev, cur) => prev === cur.professionType ? prev : undefined,
cluster[0].professionType as string | undefined,
) === undefined
) {
console.error(
"Can't find icon",
cluster,
);
}
return cluster[0].professionType;
}
async function main() {
const dataPath = Bun.argv[2];
if (dataPath === undefined) {
console.error("Data file path not specified.");
console.log(
"Usage: " + Bun.file(Bun.argv[1]).name +
" /.../.minecraft/wynntils/storage/{uuid}.data.json",
);
return;
}
console.debug(Bun.argv[2]);
const data = JSON.parse(await (Bun.file(dataPath)).text());
const {
professionNodeLocations,
}: {
professionNodeLocations: ProfessionNodeLocation[];
} = data["manager.crowdSourcedData.collectedData"].data.version210Beta2;
const clusteredProfessionNodeLocations = groupLocationItems(
professionNodeLocations,
professionNodeLocationsAreSimilar,
professionNodeLocationsClusterName,
professionNodeLocationsClusterIcon,
);
Bun.write(
"out.json",
JSON.stringify(Object.values(clusteredProfessionNodeLocations), null, 2),
);
}
main();