osrs-tools
Version:
A comprehensive TypeScript library for Old School RuneScape (OSRS) data and utilities, including quest data, skill requirements, and game item information
175 lines (174 loc) • 6.92 kB
JavaScript
import { CombatStyle, PactNodeSize, } from "./DemonicPact.model";
import { DEMONIC_PACTS_LEAGUES_7 } from "./DemonicPacts";
const STYLE_VALUES = [
CombatStyle.Universal,
CombatStyle.Melee,
CombatStyle.Ranged,
CombatStyle.Magic,
];
export function buildDemonicPactPlanningData(tree) {
const pathingByPactId = buildPathingByPactId(tree);
const pactsById = new Map(tree.pacts.map((pact) => [pact.id, pact]));
const capstonePactIds = tree.pacts.filter((pact) => pact.nodeSize === PactNodeSize.Capstone).map((pact) => pact.id);
const leafPactIds = tree.pacts
.filter((pact) => pact.id !== tree.rootNodeId && pact.linkedNodeIds.length <= 1)
.map((pact) => pact.id);
const junctionPactIds = tree.pacts.filter((pact) => pact.linkedNodeIds.length >= 3).map((pact) => pact.id);
const styleSummaries = Object.freeze(STYLE_VALUES.reduce((acc, style) => {
acc[style] = buildStyleSummary(tree, style, pathingByPactId, pactsById);
return acc;
}, {}));
return {
totalPacts: tree.pacts.length,
totalPointsAvailable: tree.totalPointsAvailable,
rootPactId: tree.rootNodeId,
capstonePactIds,
leafPactIds,
junctionPactIds,
pathingByPactId,
styleSummaries,
};
}
export const DEMONIC_PACTS_LEAGUES_7_PLANNING_DATA = buildDemonicPactPlanningData(DEMONIC_PACTS_LEAGUES_7);
function buildPathingByPactId(tree) {
const pactsById = new Map(tree.pacts.map((pact) => [pact.id, pact]));
const hops = computeMinimumHops(tree, pactsById);
const weighted = computeMinimumPointSpend(tree, pactsById);
const entries = tree.pacts.map((pact) => {
const hopInfo = hops.get(pact.id);
const weightedInfo = weighted.get(pact.id);
if (!hopInfo || !weightedInfo) {
throw new Error(`Unable to resolve pathing information for pact ${pact.id}`);
}
const pathingInfo = {
pactId: pact.id,
minimumUnlockHopsFromRoot: hopInfo.hops,
minimumPointSpendFromRoot: weightedInfo.cost,
minimumUnlockPathPactIds: weightedInfo.path,
};
return [pact.id, pathingInfo];
});
return Object.freeze(Object.fromEntries(entries));
}
function computeMinimumHops(tree, pactsById) {
const queue = [tree.rootNodeId];
const results = new Map();
results.set(tree.rootNodeId, { hops: 0, path: [tree.rootNodeId] });
while (queue.length > 0) {
const currentId = queue.shift();
if (!currentId) {
continue;
}
const current = pactsById.get(currentId);
const currentInfo = results.get(currentId);
if (!current || !currentInfo) {
continue;
}
const sortedNeighbors = [...current.linkedNodeIds].sort();
for (const neighborId of sortedNeighbors) {
if (!pactsById.has(neighborId) || results.has(neighborId)) {
continue;
}
results.set(neighborId, {
hops: currentInfo.hops + 1,
path: [...currentInfo.path, neighborId],
});
queue.push(neighborId);
}
}
return results;
}
function computeMinimumPointSpend(tree, pactsById) {
const unvisited = new Set(tree.pacts.map((pact) => pact.id));
const costs = new Map();
const paths = new Map();
for (const pact of tree.pacts) {
costs.set(pact.id, Number.POSITIVE_INFINITY);
paths.set(pact.id, []);
}
const rootPact = pactsById.get(tree.rootNodeId);
if (!rootPact) {
throw new Error(`Root pact ${tree.rootNodeId} was not found in pact tree`);
}
costs.set(rootPact.id, rootPact.pointCost);
paths.set(rootPact.id, [rootPact.id]);
while (unvisited.size > 0) {
let currentId;
let currentCost = Number.POSITIVE_INFINITY;
for (const pactId of unvisited) {
const pactCost = costs.get(pactId) ?? Number.POSITIVE_INFINITY;
if (pactCost < currentCost) {
currentCost = pactCost;
currentId = pactId;
}
}
if (!currentId || !Number.isFinite(currentCost)) {
break;
}
unvisited.delete(currentId);
const currentPact = pactsById.get(currentId);
const currentPath = paths.get(currentId) ?? [currentId];
if (!currentPact) {
continue;
}
const sortedNeighbors = [...currentPact.linkedNodeIds].sort();
for (const neighborId of sortedNeighbors) {
if (!unvisited.has(neighborId)) {
continue;
}
const neighborPact = pactsById.get(neighborId);
if (!neighborPact) {
continue;
}
const nextCost = currentCost + neighborPact.pointCost;
const prevCost = costs.get(neighborId) ?? Number.POSITIVE_INFINITY;
const nextPath = [...currentPath, neighborId];
const prevPath = paths.get(neighborId) ?? [];
if (nextCost < prevCost || (nextCost === prevCost && comparePaths(nextPath, prevPath) < 0)) {
costs.set(neighborId, nextCost);
paths.set(neighborId, nextPath);
}
}
}
const results = new Map();
for (const pact of tree.pacts) {
results.set(pact.id, {
cost: costs.get(pact.id) ?? Number.POSITIVE_INFINITY,
path: paths.get(pact.id) ?? [],
});
}
return results;
}
function comparePaths(left, right) {
if (left.length !== right.length) {
return left.length - right.length;
}
return left.join("|").localeCompare(right.join("|"));
}
function buildStyleSummary(tree, style, pathingByPactId, pactsById) {
const stylePacts = tree.pacts.filter((pact) => pact.style === style);
if (stylePacts.length === 0) {
throw new Error(`Expected at least one pact for style ${style}`);
}
let cheapestPact = stylePacts[0];
let cheapestCost = pathingByPactId[cheapestPact.id].minimumPointSpendFromRoot;
for (const pact of stylePacts.slice(1)) {
const pactCost = pathingByPactId[pact.id].minimumPointSpendFromRoot;
if (pactCost < cheapestCost || (pactCost === cheapestCost && pact.id.localeCompare(cheapestPact.id) < 0)) {
cheapestPact = pact;
cheapestCost = pactCost;
}
}
const totalPointCost = stylePacts.reduce((sum, pact) => {
const pactFromMap = pactsById.get(pact.id);
return sum + (pactFromMap?.pointCost ?? pact.pointCost);
}, 0);
return {
style,
pactCount: stylePacts.length,
capstonePactIds: stylePacts.filter((pact) => pact.nodeSize === PactNodeSize.Capstone).map((pact) => pact.id),
totalPointCost,
cheapestAccessPactId: cheapestPact.id,
cheapestAccessPointSpend: cheapestCost,
};
}