223 lines
5.6 KiB
TypeScript
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();
|