From 1409a4d274a85c6239a08b8efc5f0b5e368fe07f Mon Sep 17 00:00:00 2001 From: boli32 Date: Wed, 12 Feb 2025 01:21:41 +0000 Subject: [PATCH 1/6] Started Fix on QuestBuilder Tree --- QuestTracker/1.2/QuestTracker.js | 229 +++++++++++++------------------ 1 file changed, 98 insertions(+), 131 deletions(-) diff --git a/QuestTracker/1.2/QuestTracker.js b/QuestTracker/1.2/QuestTracker.js index eb22a3615..28a285ce2 100644 --- a/QuestTracker/1.2/QuestTracker.js +++ b/QuestTracker/1.2/QuestTracker.js @@ -103,6 +103,7 @@ var QuestTracker = QuestTracker || (function () { let QUEST_TRACKER_HISTORICAL_WEATHER = {}; let QUEST_TRACKER_WEATHER_DESCRIPTION = {}; let QUEST_TRACKER_WEATHER = true; + let QUEST_TRACKER_CACHED_QUEST_TREE = false; const loadQuestTrackerData = () => { initializeQuestTrackerState(); QUEST_TRACKER_verboseErrorLogging = state.QUEST_TRACKER.verboseErrorLogging || true; @@ -161,7 +162,8 @@ var QuestTracker = QuestTracker || (function () { precipitation: false, wind: true, visibility: true - } + }; + QUEST_TRACKER_CACHED_QUEST_TREE = state.QUEST_TRACKER.cachedQuestTree || false; }; const checkVersion = () => { if (!QUEST_TRACKER_versionChecking.TriggerConversion) Triggers.convertAutoAdvanceToTriggers(); @@ -199,6 +201,7 @@ var QuestTracker = QuestTracker || (function () { state.QUEST_TRACKER.filter = QUEST_TRACKER_FILTER; state.QUEST_TRACKER.rumourFilter = QUEST_TRACKER_RUMOUR_FILTER; state.QUEST_TRACKER.filterVisibility = QUEST_TRACKER_FILTER_Visbility; + state.QUEST_TRACKER.cachedQuestTree = QUEST_TRACKER_CACHED_QUEST_TREE; }; const initializeQuestTrackerState = (forced = false) => { if (!state.QUEST_TRACKER || Object.keys(state.QUEST_TRACKER).length === 0 || forced) { @@ -249,7 +252,8 @@ var QuestTracker = QuestTracker || (function () { }, filter: {}, rumourFilter: {}, - filterVisibility: false + filterVisibility: false, + cachedQuestTree: false }; if (!findObjs({ type: 'rollabletable', name: QUEST_TRACKER_ROLLABLETABLE_QUESTS })[0]) { const tableQuests = createObj('rollabletable', { name: QUEST_TRACKER_ROLLABLETABLE_QUESTS }); @@ -2976,141 +2980,104 @@ var QuestTracker = QuestTracker || (function () { return '#CCCCCC'; } }, - buildDAG: (questData, vars) => { + buildDAG: (questData, vars, forcedRebuild = false) => { + if (!questData || typeof questData !== 'object' || Object.keys(questData).length === 0) return {}; const questPositions = {}; + const levelMap = {}; + const parentChildMap = {}; + const childParentMap = {}; + const mutuallyExclusiveClusters = {}; const groupMap = {}; - const mutualExclusivityClusters = []; - const visitedForClusters = new Set(); - const enabledQuests = Object.keys(questData).filter((questId) => !questData[questId]?.disabled); - function findMutualExclusivityCluster(startQuestId) { - const cluster = new Set(); - const stack = [startQuestId]; - while (stack.length > 0) { - const questId = stack.pop(); - if (!cluster.has(questId)) { - cluster.add(questId); - visitedForClusters.add(questId); - const mutuallyExclusiveQuests = - questData[questId]?.relationships?.mutually_exclusive || []; - mutuallyExclusiveQuests.forEach((meQuestId) => { - if (!cluster.has(meQuestId) && enabledQuests.includes(meQuestId)) { - stack.push(meQuestId); - } - }); - } - } - return cluster; - } - enabledQuests.forEach((questId) => { - if (!visitedForClusters.has(questId)) { - const cluster = findMutualExclusivityCluster(questId); - mutualExclusivityClusters.push(cluster); - } - }); - const questIdToClusterIndex = {}; - mutualExclusivityClusters.forEach((cluster, index) => { - cluster.forEach((questId) => { - questIdToClusterIndex[questId] = index; - }); - }); - const calculateInitialLevels = (questId, visited = new Set()) => { - if (visited.has(questId)) return questData[questId].level || 0; - visited.add(questId); - const prereqs = questData[questId]?.relationships?.conditions || []; - if (prereqs.length === 0) { - questData[questId].level = 0; - return 0; - } - const prereqLevels = prereqs.map((prereq) => { - let prereqId; - if (typeof prereq === "string") { - prereqId = prereq; - } else if (typeof prereq === "object" && prereq.conditions) { - prereqId = prereq.conditions[0]; - } - return calculateInitialLevels(prereqId, new Set(visited)) + 1; + const occupiedSpaces = {}; + const xOffsetTracker = {}; + const identifyQuestGroups = () => { + Object.keys(questData).forEach(questId => { + const group = questData[questId]?.group || "default"; + if (!groupMap[group]) groupMap[group] = []; + groupMap[group].push(questId); }); - const level = Math.max(...prereqLevels); - questData[questId].level = level; - return level; }; - enabledQuests.forEach((questId) => calculateInitialLevels(questId)); - enabledQuests.forEach((questId) => { - const group = questData[questId]?.group || "Default Group"; - if (!groupMap[group]) groupMap[group] = []; - groupMap[group].push(questId); - }); - const groupWidths = {}; - const groupOrder = Object.keys(groupMap); - Object.entries(groupMap).forEach(([groupName, groupQuests]) => { - const levels = {}; - groupQuests.forEach((questId) => { - const level = questData[questId].level; - if (!levels[level]) levels[level] = []; - levels[level].push(questId); - }); - const sortedLevels = Object.keys(levels).map(Number).sort((a, b) => a - b); - let maxLevelWidth = 0; - sortedLevels.forEach((level) => { - let questsAtLevel = levels[level]; - const totalQuests = questsAtLevel.length; - const clustersAtLevel = {}; - questsAtLevel.forEach((questId) => { - const clusterIndex = questIdToClusterIndex[questId] || null; - if (clusterIndex !== null) { - if (!clustersAtLevel[clusterIndex]) clustersAtLevel[clusterIndex] = new Set(); - clustersAtLevel[clusterIndex].add(questId); - } else { - if (!clustersAtLevel["no_cluster"]) clustersAtLevel["no_cluster"] = new Set(); - clustersAtLevel["no_cluster"].add(questId); - } + const buildParentChildRelationships = () => { + Object.keys(questData).forEach(questId => { + const prereqs = questData[questId]?.relationships?.conditions || []; + prereqs.forEach(prereq => { + const prereqId = typeof prereq === "string" ? prereq : prereq?.conditions?.[0]; + if (!prereqId) return; + if (!parentChildMap[prereqId]) parentChildMap[prereqId] = []; + parentChildMap[prereqId].push(questId); + if (!childParentMap[questId]) childParentMap[questId] = []; + childParentMap[questId].push(prereqId); }); - const arrangedQuests = []; - Object.values(clustersAtLevel).forEach((cluster) => { - arrangedQuests.push(...Array.from(cluster)); + }); + }; + const calculateQuestLevels = () => { + function assignLevel(questId, depth = 0) { + if (!questId || !questData[questId]) return; + if (typeof levelMap[questId] === "number" && levelMap[questId] >= depth) return; + if (isNaN(depth)) return; + levelMap[questId] = depth; + (parentChildMap[questId] || []).forEach(childId => { + if (!questData[childId]) return; + assignLevel(childId, depth + 1); }); - levels[level] = arrangedQuests; - const levelWidth = - arrangedQuests.length * vars.ROUNDED_RECT_WIDTH + - (arrangedQuests.length - 1) * vars.HORIZONTAL_SPACING; - maxLevelWidth = Math.max(maxLevelWidth, levelWidth); + } + Object.keys(levelMap).forEach(q => delete levelMap[q]); + Object.keys(questData).forEach(questId => { + if (!childParentMap[questId]) assignLevel(questId, 0); }); - groupWidths[groupName] = maxLevelWidth; - }); - const totalTreeWidth = groupOrder.reduce((sum, groupName, index) => { - return sum + groupWidths[groupName] + (index > 0 ? vars.GROUP_SPACING : 0); - }, 0); - let cumulativeGroupWidth = -totalTreeWidth / 2; - groupOrder.forEach((groupName) => { - const groupQuests = groupMap[groupName]; - const levels = {}; - groupQuests.forEach((questId) => { - const level = questData[questId].level; - if (!levels[level]) levels[level] = []; - levels[level].push(questId); + Object.keys(questData).forEach(questId => { + if (typeof levelMap[questId] !== "number") assignLevel(questId, 0); }); - const sortedLevels = Object.keys(levels).map(Number).sort((a, b) => a - b); - sortedLevels.forEach((level) => { - let questsAtLevel = levels[level]; - const totalQuests = questsAtLevel.length; - const arrangedQuests = levels[level]; - const levelWidth = - arrangedQuests.length * vars.ROUNDED_RECT_WIDTH + - (arrangedQuests.length - 1) * vars.HORIZONTAL_SPACING; - const levelStartX = cumulativeGroupWidth + (groupWidths[groupName] - levelWidth) / 2; - arrangedQuests.forEach((questId, index) => { - const x = - levelStartX + index * (vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING); + }; + const arrangeQuestPositions = () => { + if (Object.keys(questPositions).length > 0) return; + const occupiedSpaces = {}; + const levels = [...new Set(Object.values(levelMap))].sort((a, b) => a - b); + levels.forEach(level => { + const questsAtLevel = Object.keys(levelMap).filter(q => levelMap[q] === level); + if (!occupiedSpaces[level]) occupiedSpaces[level] = new Set(); + let xOffset = 0; + questsAtLevel.forEach((questId, index) => { + while (occupiedSpaces[level].has(xOffset)) xOffset += vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING; + const x = xOffset; const y = level * (vars.ROUNDED_RECT_HEIGHT + vars.VERTICAL_SPACING); - questPositions[questId] = { - x: x, - y: y, - group: groupName, - }; + questPositions[questId] = { x, y }; + occupiedSpaces[level].add(x); + xOffset += vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING; }); }); - cumulativeGroupWidth += groupWidths[groupName] + vars.GROUP_SPACING; - }); + }; + const centerParentsOverChildren = () => { + const occupiedSpaces = {}; + Object.keys(parentChildMap).forEach(parentId => { + const children = parentChildMap[parentId] || []; + if (children.length > 0) { + const childXPositions = children.map(child => questPositions[child]?.x || 0); + if (childXPositions.length === 0) return; + const minX = Math.min(...childXPositions); + const maxX = Math.max(...childXPositions); + let parentX = (minX + maxX) / 2; + const parentY = levelMap[parentId] * (vars.ROUNDED_RECT_HEIGHT + vars.VERTICAL_SPACING); + if (!occupiedSpaces[levelMap[parentId]]) occupiedSpaces[levelMap[parentId]] = new Set(); + while (occupiedSpaces[levelMap[parentId]].has(parentX)) parentX += vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING; + occupiedSpaces[levelMap[parentId]].add(parentX); + questPositions[parentId] = { x: parentX, y: parentY }; + } + }); + }; + const saveQuestPositions = () => { + Object.keys(questData).forEach(questId => { + questData[questId].position = questPositions[questId]; + }); + QUEST_TRACKER_CACHED_QUEST_TREE = true; + Utils.updateHandoutField('quest'); + }; + identifyQuestGroups(); + buildParentChildRelationships(); + calculateQuestLevels(); + arrangeQuestPositions(); + centerParentsOverChildren(); + saveQuestPositions(); return questPositions; } }; @@ -3438,7 +3405,7 @@ var QuestTracker = QuestTracker || (function () { } } }; - const buildQuestTreeOnPage = () => { + const buildQuestTreeOnPage = (forcedRebuild = false) => { let questTreePage = findObjs({ _type: 'page', name: QUEST_TRACKER_pageName })[0]; if (!questTreePage) { errorCheck(40, 'msg', null,`Page "${QUEST_TRACKER_pageName}" not found. Please create the page manually.`); @@ -3446,7 +3413,7 @@ var QuestTracker = QuestTracker || (function () { } H.adjustPageSettings(questTreePage); H.clearPageObjects(questTreePage.id, () => { - const questPositions = H.buildDAG(QUEST_TRACKER_globalQuestData, vars); + const questPositions = H.buildDAG(QUEST_TRACKER_globalQuestData, vars, forcedRebuild); H.adjustPageSizeToFitPositions(questTreePage, questPositions); H.buildPageHeader(questTreePage); QUEST_TRACKER_TreeObjRef = {}; @@ -6639,14 +6606,14 @@ var QuestTracker = QuestTracker || (function () { }, 500); } } else if (command === '!qt-questtree') { - const { action, value } = params; + const { action, value, force = false } = params; if (errorCheck(142, 'exists', action, 'action')) return; switch (action) { case 'build': - QuestPageBuilder.buildQuestTreeOnPage(); + QuestPageBuilder.buildQuestTreeOnPage(force === true || force === 'true'); break; default: - errorCheck(143, 'msg', null,`Unknown action: ${action}`); + errorCheck(143, 'msg', null, `Unknown action: ${action}`); break; } } From 2ebbc3181bf3d2c7a1856ae4622f577416e9dbbc Mon Sep 17 00:00:00 2001 From: boli32 Date: Wed, 12 Feb 2025 01:48:51 +0000 Subject: [PATCH 2/6] fixed alignment --- QuestTracker/1.2/QuestTracker.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/QuestTracker/1.2/QuestTracker.js b/QuestTracker/1.2/QuestTracker.js index 28a285ce2..8633e48fa 100644 --- a/QuestTracker/1.2/QuestTracker.js +++ b/QuestTracker/1.2/QuestTracker.js @@ -2890,7 +2890,8 @@ var QuestTracker = QuestTracker || (function () { DEFAULT_STATUS_COLOR: '#000000', QUESTICON_WIDTH: 305, GROUP_SPACING: 800, - QUESTICON_HEIGHT: 92 + QUESTICON_HEIGHT: 92, + PAGE_X_OFFSET: 360 }; const H = { adjustPageSettings: (page) => { @@ -3090,7 +3091,7 @@ var QuestTracker = QuestTracker || (function () { errorCheck(32, 'msg', null,`Quest data for "${questId}" is missing.`); return; } - const x = position.x + totalWidth / 2; + const x = position.x + vars.PAGE_X_OFFSET; const y = position.y + vars.PAGE_HEADER_HEIGHT + vars.VERTICAL_SPACING; const isHidden = questData.hidden || false; D.drawQuestGraphics(questId, questData, page.id, x, y, isHidden); @@ -3132,7 +3133,7 @@ var QuestTracker = QuestTracker || (function () { errorCheck(35, 'msg', null,`Quest data for "${questId}" is missing.`); return; } - const x = position.x + totalWidth / 2; + const x = position.x + vars.PAGE_X_OFFSET; const y = position.y + vars.PAGE_HEADER_HEIGHT + vars.VERTICAL_SPACING; const isHidden = questData.hidden || false; const textLayer = isHidden ? 'gmlayer' : 'objects'; @@ -3154,7 +3155,7 @@ var QuestTracker = QuestTracker || (function () { drawQuestConnections: (pageId, questPositions) => { const page = getObj('page', pageId); const pageWidth = page.get('width') * vars.DEFAULT_PAGE_UNIT; - const offsetX = pageWidth / 2; + const offsetX = vars.PAGE_X_OFFSET; const incomingPaths = {}; Object.entries(questPositions).forEach(([questId, position]) => { const questData = QUEST_TRACKER_globalQuestData[questId]; @@ -3269,7 +3270,7 @@ var QuestTracker = QuestTracker || (function () { drawMutuallyExclusiveConnections: (pageId, questPositions) => { const page = getObj('page', pageId); const pageWidth = page.get('width') * vars.DEFAULT_PAGE_UNIT; - const offsetX = pageWidth / 2; + const offsetX = vars.PAGE_X_OFFSET; const mutualExclusions = []; Object.entries(QUEST_TRACKER_globalQuestData).forEach(([questId, questData]) => { const mutuallyExclusiveWith = questData.relationships?.mutually_exclusive || []; From 6eba34e9537f8b3e6444f8df59a11a8bd5ca58fa Mon Sep 17 00:00:00 2001 From: boli32 Date: Fri, 14 Feb 2025 10:48:04 +0000 Subject: [PATCH 3/6] change to tetris connections in questbuilder tree --- QuestTracker/1.2/QuestTracker.js | 334 +++++++++++++++++++++---------- 1 file changed, 233 insertions(+), 101 deletions(-) diff --git a/QuestTracker/1.2/QuestTracker.js b/QuestTracker/1.2/QuestTracker.js index 8633e48fa..55e0dfaf4 100644 --- a/QuestTracker/1.2/QuestTracker.js +++ b/QuestTracker/1.2/QuestTracker.js @@ -2981,106 +2981,238 @@ var QuestTracker = QuestTracker || (function () { return '#CCCCCC'; } }, - buildDAG: (questData, vars, forcedRebuild = false) => { - if (!questData || typeof questData !== 'object' || Object.keys(questData).length === 0) return {}; - const questPositions = {}; - const levelMap = {}; - const parentChildMap = {}; - const childParentMap = {}; - const mutuallyExclusiveClusters = {}; - const groupMap = {}; - const occupiedSpaces = {}; - const xOffsetTracker = {}; - const identifyQuestGroups = () => { - Object.keys(questData).forEach(questId => { - const group = questData[questId]?.group || "default"; - if (!groupMap[group]) groupMap[group] = []; - groupMap[group].push(questId); - }); - }; - const buildParentChildRelationships = () => { - Object.keys(questData).forEach(questId => { - const prereqs = questData[questId]?.relationships?.conditions || []; - prereqs.forEach(prereq => { - const prereqId = typeof prereq === "string" ? prereq : prereq?.conditions?.[0]; - if (!prereqId) return; - if (!parentChildMap[prereqId]) parentChildMap[prereqId] = []; - parentChildMap[prereqId].push(questId); - if (!childParentMap[questId]) childParentMap[questId] = []; - childParentMap[questId].push(prereqId); - }); - }); - }; - const calculateQuestLevels = () => { - function assignLevel(questId, depth = 0) { - if (!questId || !questData[questId]) return; - if (typeof levelMap[questId] === "number" && levelMap[questId] >= depth) return; - if (isNaN(depth)) return; - levelMap[questId] = depth; - (parentChildMap[questId] || []).forEach(childId => { - if (!questData[childId]) return; - assignLevel(childId, depth + 1); - }); - } - Object.keys(levelMap).forEach(q => delete levelMap[q]); - Object.keys(questData).forEach(questId => { - if (!childParentMap[questId]) assignLevel(questId, 0); - }); - Object.keys(questData).forEach(questId => { - if (typeof levelMap[questId] !== "number") assignLevel(questId, 0); - }); - }; - const arrangeQuestPositions = () => { - if (Object.keys(questPositions).length > 0) return; - const occupiedSpaces = {}; - const levels = [...new Set(Object.values(levelMap))].sort((a, b) => a - b); - levels.forEach(level => { - const questsAtLevel = Object.keys(levelMap).filter(q => levelMap[q] === level); - if (!occupiedSpaces[level]) occupiedSpaces[level] = new Set(); - let xOffset = 0; - questsAtLevel.forEach((questId, index) => { - while (occupiedSpaces[level].has(xOffset)) xOffset += vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING; - const x = xOffset; - const y = level * (vars.ROUNDED_RECT_HEIGHT + vars.VERTICAL_SPACING); - questPositions[questId] = { x, y }; - occupiedSpaces[level].add(x); - xOffset += vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING; - }); - }); - }; - const centerParentsOverChildren = () => { - const occupiedSpaces = {}; - Object.keys(parentChildMap).forEach(parentId => { - const children = parentChildMap[parentId] || []; - if (children.length > 0) { - const childXPositions = children.map(child => questPositions[child]?.x || 0); - if (childXPositions.length === 0) return; - const minX = Math.min(...childXPositions); - const maxX = Math.max(...childXPositions); - let parentX = (minX + maxX) / 2; - const parentY = levelMap[parentId] * (vars.ROUNDED_RECT_HEIGHT + vars.VERTICAL_SPACING); - if (!occupiedSpaces[levelMap[parentId]]) occupiedSpaces[levelMap[parentId]] = new Set(); - while (occupiedSpaces[levelMap[parentId]].has(parentX)) parentX += vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING; - occupiedSpaces[levelMap[parentId]].add(parentX); - questPositions[parentId] = { x: parentX, y: parentY }; - } - }); - }; - const saveQuestPositions = () => { - Object.keys(questData).forEach(questId => { - questData[questId].position = questPositions[questId]; - }); - QUEST_TRACKER_CACHED_QUEST_TREE = true; - Utils.updateHandoutField('quest'); - }; - identifyQuestGroups(); - buildParentChildRelationships(); - calculateQuestLevels(); - arrangeQuestPositions(); - centerParentsOverChildren(); - saveQuestPositions(); - return questPositions; - } + + + + + + + + + + + + + + + + + + + + + + + +buildDAG: (questData, vars, forcedRebuild = false) => { + if (!questData || typeof questData !== 'object' || Object.keys(questData).length === 0) return {}; + + // Data Structures + const questPositions = {}; + const parentChildMap = {}; + const childParentMap = {}; + const layers = []; + const nodeLayerMap = {}; + const dummyNodes = []; + let dummyNodeId = 0; + + // Step 1: Cycle Removal + function removeCycles() { + const visited = new Set(); + const stack = new Set(); + const reversedEdges = []; + + function visit(node) { + if (stack.has(node)) { + return true; // Cycle detected + } + if (visited.has(node)) { + return false; + } + visited.add(node); + stack.add(node); + const children = parentChildMap[node] || []; + for (const child of children) { + if (visit(child)) { + reversedEdges.push([child, node]); + // Reverse the edge + parentChildMap[child] = parentChildMap[child] || []; + parentChildMap[child].push(node); + childParentMap[node] = childParentMap[node] || []; + childParentMap[node].push(child); + } + } + stack.delete(node); + return false; + } + + for (const node in questData) { + visit(node); + } + + // Remove reversed edges from original maps + for (const [from, to] of reversedEdges) { + parentChildMap[to] = parentChildMap[to].filter(child => child !== from); + if (parentChildMap[to].length === 0) { + delete parentChildMap[to]; + } + childParentMap[from] = childParentMap[from].filter(parent => parent !== to); + if (childParentMap[from].length === 0) { + delete childParentMap[from]; + } + } + } + + // Step 2: Layer Assignment + function assignLayers() { + const inDegree = {}; + const zeroInDegree = []; + + // Initialize in-degree count + for (const node in questData) { + inDegree[node] = (childParentMap[node] || []).length; + if (inDegree[node] === 0) { + zeroInDegree.push(node); + } + } + + // Kahn's algorithm for topological sorting + while (zeroInDegree.length > 0) { + const node = zeroInDegree.shift(); + const layer = nodeLayerMap[node] || 0; + layers[layer] = layers[layer] || []; + layers[layer].push(node); + const children = parentChildMap[node] || []; + for (const child of children) { + inDegree[child]--; + if (inDegree[child] === 0) { + zeroInDegree.push(child); + nodeLayerMap[child] = layer + 1; + } + } + } + } + + // Step 3: Crossing Reduction + function reduceCrossings() { + const barycenter = (layer, nodeOrder) => { + const positions = {}; + nodeOrder.forEach((node, index) => { + positions[node] = index; + }); + const barycenters = {}; + for (const node of nodeOrder) { + const parents = childParentMap[node] || []; + if (parents.length > 0) { + const avg = parents.reduce((sum, p) => sum + (positions[p] || 0), 0) / parents.length; + barycenters[node] = avg; + } else { + barycenters[node] = positions[node]; + } + } + return barycenters; + }; + + const sortByBarycenter = (layer, nodeOrder) => { + const barycenters = barycenter(layer, nodeOrder); + nodeOrder.sort((a, b) => barycenters[a] - barycenters[b]); + }; + + for (let i = 1; i < layers.length; i++) { + sortByBarycenter(i, layers[i]); + } + } + + // Step 4: Coordinate Assignment + function assignCoordinates() { + const layerHeights = layers.map(layer => layer.length); + const maxLayerHeight = Math.max(...layerHeights); + const layerY = []; + let currentY = 0; + + for (let i = 0; i < layers.length; i++) { + layerY[i] = currentY; + currentY += vars.ROUNDED_RECT_HEIGHT + vars.VERTICAL_SPACING; + } + + for (let i = 0; i < layers.length; i++) { + const layer = layers[i]; + const totalWidth = layer.length * (vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING); + let currentX = -totalWidth / 2; + + for (const node of layer) { + questPositions[node] = { + x: currentX, + y: layerY[i] + }; + currentX += vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING; + } + } + } + + // Build Parent-Child Relationships + function buildRelationships() { + for (const questId in questData) { + const prereqs = questData[questId]?.relationships?.conditions || []; + for (const prereq of prereqs) { + const prereqId = typeof prereq === 'string' ? prereq : prereq?.conditions?.[0]; + if (!prereqId) continue; + parentChildMap[prereqId] = parentChildMap[prereqId] || []; + parentChildMap[prereqId].push(questId); + childParentMap[questId] = childParentMap[questId] || []; + childParentMap[questId].push(prereqId); + } + } + } + + // Main Execution + buildRelationships(); + removeCycles(); + assignLayers(); + reduceCrossings(); + assignCoordinates(); + + return questPositions; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + }; const D = { drawQuestTreeFromPositions: (page, questPositions, callback) => { @@ -5324,7 +5456,7 @@ var QuestTracker = QuestTracker || (function () { menu += `

Data

${RefreshImport} JSON Data`; menu += `
Check Version`; menu += `
Reset to Defaults`; - menu += `

Quest Tree

Build Quest Tree Page`; + menu += `

Quest Tree

Build Quest Tree Page`; menu += `

Calander

Calendar: ${CALENDARS[QUEST_TRACKER_calenderType]?.name || "Unknown Calendar"}`; menu += `

Weather


Toggle Weather (${QUEST_TRACKER_WEATHER === true ? 'on' : 'off'})`; if (QUEST_TRACKER_WEATHER) { From cd87c54468e9056a5022b8a38c392f13c280297f Mon Sep 17 00:00:00 2001 From: boli32 Date: Mon, 17 Feb 2025 11:40:27 +0000 Subject: [PATCH 4/6] Finished Adjustment of Quest Tracker Tree --- QuestTracker/1.2/QuestTracker.js | 378 ++++++++++++++----------------- 1 file changed, 171 insertions(+), 207 deletions(-) diff --git a/QuestTracker/1.2/QuestTracker.js b/QuestTracker/1.2/QuestTracker.js index 55e0dfaf4..156877e4c 100644 --- a/QuestTracker/1.2/QuestTracker.js +++ b/QuestTracker/1.2/QuestTracker.js @@ -2891,7 +2891,9 @@ var QuestTracker = QuestTracker || (function () { QUESTICON_WIDTH: 305, GROUP_SPACING: 800, QUESTICON_HEIGHT: 92, - PAGE_X_OFFSET: 360 + PAGE_X_OFFSET: 360, + HORIZONTAL_GROUP_SPACING: 100, + CANVAS_PADDING: 100 }; const H = { adjustPageSettings: (page) => { @@ -2981,238 +2983,200 @@ var QuestTracker = QuestTracker || (function () { return '#CCCCCC'; } }, - - - - - - - - - - - - - - - - - - - - + buildDAG: (questData, vars, forcedRebuild = false) => { + if (!questData || typeof questData !== 'object' || Object.keys(questData).length === 0) return {}; + const questPositions = {}; + const parentChildMap = {}; + const childParentMap = {}; + const layers = {}; + const nodeLayerMap = {}; + const dummyNodes = []; + let dummyNodeId = 0; + const groupOffsets = {}; + const groupMaxWidths = {}; + const horizontalSpacing = vars.HORIZONTAL_GROUP_SPACING; + const groupMap = {}; + // **If not forcedRebuild, load existing positions and return them** + if (!forcedRebuild) { + Object.keys(questData).forEach(questId => { + if (questData[questId].position) { + questPositions[questId] = { ...questData[questId].position }; + } + }); + return questPositions; + } -buildDAG: (questData, vars, forcedRebuild = false) => { - if (!questData || typeof questData !== 'object' || Object.keys(questData).length === 0) return {}; + // **Organize quests by group** + for (const questId in questData) { + const group = questData[questId].group || 'default'; + if (!groupMap[group]) { + groupMap[group] = []; + } + groupMap[group].push(questId); + } - // Data Structures - const questPositions = {}; - const parentChildMap = {}; - const childParentMap = {}; - const layers = []; - const nodeLayerMap = {}; - const dummyNodes = []; - let dummyNodeId = 0; + const removeCycles = (group) => { + const visited = new Set(); + const stack = new Set(); + const reversedEdges = []; - // Step 1: Cycle Removal - function removeCycles() { - const visited = new Set(); - const stack = new Set(); - const reversedEdges = []; + function visit(node) { + if (stack.has(node)) return true; + if (visited.has(node)) return false; + visited.add(node); + stack.add(node); + const children = parentChildMap[node] || []; + for (const child of children) { + if (visit(child)) { + reversedEdges.push([child, node]); + parentChildMap[child] = parentChildMap[child] || []; + parentChildMap[child].push(node); + childParentMap[node] = childParentMap[node] || []; + childParentMap[node].push(child); + } + } + stack.delete(node); + return false; + } - function visit(node) { - if (stack.has(node)) { - return true; // Cycle detected - } - if (visited.has(node)) { - return false; - } - visited.add(node); - stack.add(node); - const children = parentChildMap[node] || []; - for (const child of children) { - if (visit(child)) { - reversedEdges.push([child, node]); - // Reverse the edge - parentChildMap[child] = parentChildMap[child] || []; - parentChildMap[child].push(node); - childParentMap[node] = childParentMap[node] || []; - childParentMap[node].push(child); - } - } - stack.delete(node); - return false; - } + for (const node of groupMap[group]) { + visit(node); + } - for (const node in questData) { - visit(node); - } + for (const [from, to] of reversedEdges) { + parentChildMap[to] = parentChildMap[to].filter(child => child !== from); + if (parentChildMap[to].length === 0) delete parentChildMap[to]; + childParentMap[from] = childParentMap[from].filter(parent => parent !== to); + if (childParentMap[from].length === 0) delete childParentMap[from]; + } + }; - // Remove reversed edges from original maps - for (const [from, to] of reversedEdges) { - parentChildMap[to] = parentChildMap[to].filter(child => child !== from); - if (parentChildMap[to].length === 0) { - delete parentChildMap[to]; - } - childParentMap[from] = childParentMap[from].filter(parent => parent !== to); - if (childParentMap[from].length === 0) { - delete childParentMap[from]; - } - } - } + const assignLayers = (group) => { + const inDegree = {}; + const zeroInDegree = []; + layers[group] = []; - // Step 2: Layer Assignment - function assignLayers() { - const inDegree = {}; - const zeroInDegree = []; + for (const node of groupMap[group]) { + inDegree[node] = (childParentMap[node] || []).length; + if (inDegree[node] === 0) { + zeroInDegree.push(node); + } + } - // Initialize in-degree count - for (const node in questData) { - inDegree[node] = (childParentMap[node] || []).length; - if (inDegree[node] === 0) { - zeroInDegree.push(node); - } - } + while (zeroInDegree.length > 0) { + const node = zeroInDegree.shift(); + const layer = nodeLayerMap[node] || 0; + layers[group][layer] = layers[group][layer] || []; + layers[group][layer].push(node); + const children = parentChildMap[node] || []; + for (const child of children) { + inDegree[child]--; + if (inDegree[child] === 0) { + zeroInDegree.push(child); + nodeLayerMap[child] = layer + 1; + } + } + } + }; - // Kahn's algorithm for topological sorting - while (zeroInDegree.length > 0) { - const node = zeroInDegree.shift(); - const layer = nodeLayerMap[node] || 0; - layers[layer] = layers[layer] || []; - layers[layer].push(node); - const children = parentChildMap[node] || []; - for (const child of children) { - inDegree[child]--; - if (inDegree[child] === 0) { - zeroInDegree.push(child); - nodeLayerMap[child] = layer + 1; - } - } - } - } + const assignCoordinates = (group, offsetX) => { + const layerHeights = layers[group].map(layer => layer.length); + const maxLayerHeight = Math.max(...layerHeights); + const layerY = []; + let currentY = 0; - // Step 3: Crossing Reduction - function reduceCrossings() { - const barycenter = (layer, nodeOrder) => { - const positions = {}; - nodeOrder.forEach((node, index) => { - positions[node] = index; - }); - const barycenters = {}; - for (const node of nodeOrder) { - const parents = childParentMap[node] || []; - if (parents.length > 0) { - const avg = parents.reduce((sum, p) => sum + (positions[p] || 0), 0) / parents.length; - barycenters[node] = avg; - } else { - barycenters[node] = positions[node]; - } - } - return barycenters; - }; + for (let i = 0; i < layers[group].length; i++) { + layerY[i] = currentY; + currentY += vars.ROUNDED_RECT_HEIGHT + vars.VERTICAL_SPACING; + } - const sortByBarycenter = (layer, nodeOrder) => { - const barycenters = barycenter(layer, nodeOrder); - nodeOrder.sort((a, b) => barycenters[a] - barycenters[b]); - }; + let maxWidth = 0; + const newPositions = {}; - for (let i = 1; i < layers.length; i++) { - sortByBarycenter(i, layers[i]); - } - } + for (let i = 0; i < layers[group].length; i++) { + const layer = layers[group][i]; + if (!layer || layer.length === 0) continue; + const totalWidth = layer.length * (vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING); + let currentX = (-totalWidth / 2) + offsetX; - // Step 4: Coordinate Assignment - function assignCoordinates() { - const layerHeights = layers.map(layer => layer.length); - const maxLayerHeight = Math.max(...layerHeights); - const layerY = []; - let currentY = 0; + for (const node of layer) { + newPositions[node] = { + x: isNaN(currentX) ? offsetX : currentX, + y: isNaN(layerY[i]) ? 0 : layerY[i] + }; + currentX += vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING; + maxWidth = Math.max(maxWidth, currentX); + } + } - for (let i = 0; i < layers.length; i++) { - layerY[i] = currentY; - currentY += vars.ROUNDED_RECT_HEIGHT + vars.VERTICAL_SPACING; - } + Object.assign(questPositions, newPositions); + groupMaxWidths[group] = maxWidth; + }; - for (let i = 0; i < layers.length; i++) { - const layer = layers[i]; - const totalWidth = layer.length * (vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING); - let currentX = -totalWidth / 2; + const buildRelationships = (group) => { + for (const questId of groupMap[group]) { + const prereqs = questData[questId]?.relationships?.conditions || []; + for (const prereq of prereqs) { + const prereqId = typeof prereq === 'string' ? prereq : prereq?.conditions?.[0]; + if (!prereqId) continue; + parentChildMap[prereqId] = parentChildMap[prereqId] || []; + parentChildMap[prereqId].push(questId); + childParentMap[questId] = childParentMap[questId] || []; + childParentMap[questId].push(prereqId); + } + } + }; - for (const node of layer) { - questPositions[node] = { - x: currentX, - y: layerY[i] - }; - currentX += vars.ROUNDED_RECT_WIDTH + vars.HORIZONTAL_SPACING; - } - } - } + const shiftXCoordinates = () => { + let minX = Infinity; + let maxX = -Infinity; + Object.values(questPositions).forEach(pos => { + if (pos.x < minX) minX = pos.x; + if (pos.x > maxX) maxX = pos.x; + }); + const totalOffset = minX < 0 ? Math.abs(minX) + vars.CANVAS_PADDING : 0; + Object.values(questPositions).forEach(pos => { + pos.x += totalOffset; + }); + }; - // Build Parent-Child Relationships - function buildRelationships() { - for (const questId in questData) { - const prereqs = questData[questId]?.relationships?.conditions || []; - for (const prereq of prereqs) { - const prereqId = typeof prereq === 'string' ? prereq : prereq?.conditions?.[0]; - if (!prereqId) continue; - parentChildMap[prereqId] = parentChildMap[prereqId] || []; - parentChildMap[prereqId].push(questId); - childParentMap[questId] = childParentMap[questId] || []; - childParentMap[questId].push(prereqId); - } - } - } + const saveQuestPositions = () => { + Object.keys(questData).forEach(questId => { + // **Ensure manual positions are respected** + if (questData[questId].position?.manual === true) { + questPositions[questId] = { ...questData[questId].position }; + } else { + // Add position if missing and set manual to false by default + if (!questData[questId].position) { + questData[questId].position = { manual: false }; + } + questData[questId].position = { ...questPositions[questId], manual: false }; + } + }); + QUEST_TRACKER_CACHED_QUEST_TREE = true; + Utils.updateHandoutField('quest'); + }; - // Main Execution - buildRelationships(); - removeCycles(); - assignLayers(); - reduceCrossings(); - assignCoordinates(); + let offsetX = 0; + for (const group in groupMap) { + buildRelationships(group); + removeCycles(group); + assignLayers(group); + assignCoordinates(group, offsetX); + groupOffsets[group] = offsetX; + offsetX += groupMaxWidths[group] + horizontalSpacing; + } - return questPositions; + shiftXCoordinates(); + saveQuestPositions(); + return questPositions; } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - }; const D = { drawQuestTreeFromPositions: (page, questPositions, callback) => { From 9913b39abc7044e7e0017c70f1502920580d281c Mon Sep 17 00:00:00 2001 From: boli32 Date: Mon, 17 Feb 2025 12:03:36 +0000 Subject: [PATCH 5/6] Updated readme to v1.21 --- QuestTracker/README.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/QuestTracker/README.md b/QuestTracker/README.md index 29c48992d..17032b1f4 100644 --- a/QuestTracker/README.md +++ b/QuestTracker/README.md @@ -298,6 +298,20 @@ Quest can have images which are tokens. These are set manually using the rollabl Simply use the command **!gmnote** (create it as a token macro) when selecting a quest token and it will open up an small menu with quick functionality with the main interface. actioning any of these commands will open up the full quest interface afterwards. I highly recomend using **!gmnote --config** to toggle off the footer buttons when you set it up. +### Manually adjustments of Quest Positions. + +``` + "position": { + "x": 4960, + "y": 0, + "manual": false + } +``` +Under each quest, there is a manual option to adjust the placement of the quests. If manual is set to true, the Quest Tracker script will use the specified positions rather than the calculated ones. Running the command **!qt-import** will load these variables into the environment. + +> Note: It is advisable to build the entire quest structure for a campaign before making these final changes, as you may find stray quests placed in undesirable positions. The manual tweaks are best used as a 'finishing' tool. + + ## Triggers Module @@ -462,7 +476,7 @@ Select a calendar type from the configuration menu. The system will automaticall Users can add custom calanders by editing the **QuestTracker Calendar** Handout; as with other QuestTracker handouts all data is stored in the GM Notes field; the structure used is below as an example along with explanations for each field. -*NOTE: it is very easy to mess this object up, so be careful. use a ![JSON Validator](https://jsonlint.com/) to confirm it is a valid object before refreshing the JSON files in the configuration settings.* +> NOTE: it is very easy to mess this object up, so be careful. use a ![JSON Validator](https://jsonlint.com/) to confirm it is a valid object before refreshing the JSON files in the configuration settings. ``` { @@ -678,7 +692,7 @@ Users can add custom calanders by editing the **QuestTracker Calendar** Handout; ``` - **Usage**: Provides seasonal and environmental context. -*NOTE: Keeping values between -20 and 20 will allow the weather module to perform correctly.* +> NOTE: Keeping values between -20 and 20 will allow the weather module to perform correctly. ## Event Module @@ -740,6 +754,11 @@ No, there is a script in place to convert all autoadvance triggers into the new ## Updates +#### 2024-02-18 +* Release of **v1.2.1**; + * The Quest Tree has been overhauled on the back end so quests are placed better + * Added option to maually edit the positioning + #### 2025-02-11 * Release of **v1.2**; Rumours Module has been overhauled - **Rumours Module** From 2b40b1a7f0923c20a61f6e6b6fe46c9c5cacb23f Mon Sep 17 00:00:00 2001 From: boli32 Date: Mon, 17 Feb 2025 12:49:35 +0000 Subject: [PATCH 6/6] fixed header --- QuestTracker/1.2/QuestTracker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/QuestTracker/1.2/QuestTracker.js b/QuestTracker/1.2/QuestTracker.js index 156877e4c..d02eb82b5 100644 --- a/QuestTracker/1.2/QuestTracker.js +++ b/QuestTracker/1.2/QuestTracker.js @@ -1,7 +1,7 @@ -// Github: https://github.com/boli32/QuestTracker/blob/main/QuestTracker.js +// Github: https://github.com/Roll20/roll20-api-scripts/tree/master/QuestTracker/ // By: Boli (Steven Wrighton): Professional Software Developer, Enthusiatic D&D Player since 1993. // Contact: https://app.roll20.net/users/3714078/boli -// Readme https://github.com/boli32/QuestTracker/blob/main/README.md +// Readme https://github.com/Roll20/roll20-api-scripts/blob/master/QuestTracker/README.md var QuestTracker = QuestTracker || (function () {