UNPKG

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
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, }; }