From 58e70eb49de4a4d773cd3c1621ac28452afa4977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Neumair=20G=C3=BCnther?= Date: Fri, 5 Sep 2025 19:26:08 +0200 Subject: [PATCH 1/2] Replace sorted table with SVG visualization --- index.html | 3 + sorted.html | 24 ++++++++ sorted.js | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 45 +++++++++++++++ 4 files changed, 226 insertions(+) create mode 100644 sorted.html create mode 100644 sorted.js diff --git a/index.html b/index.html index c28ce1f..8a6b077 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,9 @@

World Tech Tree (MVP)

+
diff --git a/sorted.html b/sorted.html new file mode 100644 index 0000000..0c1dd4a --- /dev/null +++ b/sorted.html @@ -0,0 +1,24 @@ + + + + + + Sorted Technologies + + + + +
+

Sorted Technologies

+ +
+
+
+ +
+
+ + + diff --git a/sorted.js b/sorted.js new file mode 100644 index 0000000..8fda7c6 --- /dev/null +++ b/sorted.js @@ -0,0 +1,154 @@ +// Render technologies in a simple SVG grid sorted by era and dependency level + +document.addEventListener('DOMContentLoaded', async () => { + let data = []; + try { + const resp = await fetch('/api/tech-tree'); + if (resp.ok) { + data = await resp.json(); + } else { + throw new Error('Failed to load tech tree'); + } + } catch (err) { + console.error('Error loading tech tree:', err); + return; + } + + // Build adjacency map and compute dependency levels + const dependentsMap = {}; + const levelMap = {}; + data.forEach(t => { + (t.prerequisites || []).forEach(pr => { + if (!dependentsMap[pr]) dependentsMap[pr] = []; + dependentsMap[pr].push(t.id); + }); + }); + const queue = []; + data.forEach(t => { + if (!t.prerequisites || t.prerequisites.length === 0) { + levelMap[t.id] = 0; + queue.push(t.id); + } + }); + while (queue.length) { + const current = queue.shift(); + const currentLevel = levelMap[current]; + (dependentsMap[current] || []).forEach(dep => { + const nextLevel = currentLevel + 1; + if (levelMap[dep] === undefined || nextLevel < levelMap[dep]) { + levelMap[dep] = nextLevel; + queue.push(dep); + } + }); + } + data.forEach(t => { + if (levelMap[t.id] === undefined) levelMap[t.id] = 0; + t.level = levelMap[t.id]; + }); + + const eraOrder = { + Ancient: 0, + Classical: 1, + Medieval: 2, + Renaissance: 3, + Industrial: 4, + Modern: 5, + Future: 6 + }; + + const eraColors = { + Ancient: '#e67e22', + Classical: '#3498db', + Medieval: '#2ecc71', + Renaissance: '#9b59b6', + Industrial: '#f1c40f', + Modern: '#e74c3c', + Future: '#95a5a6' + }; + + const nodeWidth = 120; + const nodeHeight = 40; + const xSpacing = 200; + const ySpacing = 80; + const margin = 40; + + const maxLevel = Math.max(...data.map(t => t.level), 0); + const maxEraIndex = Math.max(...data.map(t => eraOrder[t.era] ?? 7), 0); + + const svg = document.getElementById('tech-svg'); + svg.setAttribute('width', margin * 2 + (maxEraIndex + 1) * xSpacing); + svg.setAttribute('height', margin * 2 + (maxLevel + 1) * ySpacing); + + // Arrow head definition + const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); + const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker'); + marker.setAttribute('id', 'arrow'); + marker.setAttribute('viewBox', '0 0 10 10'); + marker.setAttribute('refX', '10'); + marker.setAttribute('refY', '5'); + marker.setAttribute('markerWidth', '6'); + marker.setAttribute('markerHeight', '6'); + marker.setAttribute('orient', 'auto'); + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z'); + path.setAttribute('fill', '#999'); + marker.appendChild(path); + defs.appendChild(marker); + svg.appendChild(defs); + + const linkGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + svg.appendChild(linkGroup); + const nodeGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + svg.appendChild(nodeGroup); + + const positions = {}; + data.forEach(t => { + const eraIdx = eraOrder[t.era] ?? 7; + const x = margin + eraIdx * xSpacing; + const y = margin + t.level * ySpacing; + positions[t.id] = { x, y }; + }); + + // Draw links + data.forEach(t => { + (t.prerequisites || []).forEach(pr => { + const src = positions[pr]; + const dst = positions[t.id]; + if (!src || !dst) return; + const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + line.setAttribute('class', 'tech-link'); + line.setAttribute('x1', src.x + nodeWidth / 2); + line.setAttribute('y1', src.y + nodeHeight / 2); + line.setAttribute('x2', dst.x + nodeWidth / 2); + line.setAttribute('y2', dst.y + nodeHeight / 2); + line.setAttribute('marker-end', 'url(#arrow)'); + linkGroup.appendChild(line); + }); + }); + + // Draw nodes + data.forEach(t => { + const pos = positions[t.id]; + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + g.setAttribute('class', 'tech-node'); + g.setAttribute('transform', `translate(${pos.x},${pos.y})`); + + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.setAttribute('width', nodeWidth); + rect.setAttribute('height', nodeHeight); + rect.setAttribute('rx', 6); + rect.setAttribute('fill', eraColors[t.era] || '#cccccc'); + g.appendChild(rect); + + const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + text.setAttribute('x', nodeWidth / 2); + text.setAttribute('y', nodeHeight / 2); + text.setAttribute('text-anchor', 'middle'); + text.setAttribute('dominant-baseline', 'middle'); + text.textContent = t.name; + g.appendChild(text); + + nodeGroup.appendChild(g); + }); +}); + diff --git a/style.css b/style.css index cb6c3b4..bb8a51b 100644 --- a/style.css +++ b/style.css @@ -17,6 +17,20 @@ header { box-shadow: 0 2px 4px rgba(0,0,0,0.1); } +header nav { + margin-top: 10px; +} + +header nav a { + color: #fff; + margin: 0 10px; + text-decoration: none; +} + +header nav a:hover { + text-decoration: underline; +} + main { display: flex; flex-grow: 1; @@ -44,6 +58,37 @@ main { height: 80vh; /* Ensure the canvas has some initial height */ } +#sorted-tech-container { + flex: 1; + border: 1px solid #ddd; + border-radius: 8px; + background-color: #fff; + box-shadow: 0 2px 6px rgba(0,0,0,0.05); + overflow: auto; + position: relative; +} + +#tech-svg { + width: 100%; + height: 100%; +} + +.tech-node rect { + stroke: #2c3e50; + stroke-width: 1; +} + +.tech-node text { + font-size: 12px; + fill: #fff; + pointer-events: none; +} + +.tech-link { + stroke: #999; + stroke-width: 1.5; +} + #tech-info-panel { border: 1px solid #ddd; border-radius: 8px; From 21066534913b5f71df9f917a76d067f21fc3b7f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Neumair=20G=C3=BCnther?= Date: Fri, 5 Sep 2025 19:30:38 +0200 Subject: [PATCH 2/2] Show technologies sorted by era and dependency --- sorted.html | 12 +++++- sorted.js | 114 +++++++++++++--------------------------------------- style.css | 25 +++++------- 3 files changed, 49 insertions(+), 102 deletions(-) diff --git a/sorted.html b/sorted.html index 0c1dd4a..6d29f84 100644 --- a/sorted.html +++ b/sorted.html @@ -16,7 +16,17 @@

Sorted Technologies

- + + + + + + + + + + +
EraLevelNamePrerequisites
diff --git a/sorted.js b/sorted.js index 8fda7c6..c9a116d 100644 --- a/sorted.js +++ b/sorted.js @@ -1,4 +1,4 @@ -// Render technologies in a simple SVG grid sorted by era and dependency level +// Render a table of technologies sorted by era and dependency level document.addEventListener('DOMContentLoaded', async () => { let data = []; @@ -17,12 +17,14 @@ document.addEventListener('DOMContentLoaded', async () => { // Build adjacency map and compute dependency levels const dependentsMap = {}; const levelMap = {}; + data.forEach(t => { (t.prerequisites || []).forEach(pr => { if (!dependentsMap[pr]) dependentsMap[pr] = []; dependentsMap[pr].push(t.id); }); }); + const queue = []; data.forEach(t => { if (!t.prerequisites || t.prerequisites.length === 0) { @@ -30,6 +32,7 @@ document.addEventListener('DOMContentLoaded', async () => { queue.push(t.id); } }); + while (queue.length) { const current = queue.shift(); const currentLevel = levelMap[current]; @@ -41,6 +44,7 @@ document.addEventListener('DOMContentLoaded', async () => { } }); } + data.forEach(t => { if (levelMap[t.id] === undefined) levelMap[t.id] = 0; t.level = levelMap[t.id]; @@ -56,99 +60,35 @@ document.addEventListener('DOMContentLoaded', async () => { Future: 6 }; - const eraColors = { - Ancient: '#e67e22', - Classical: '#3498db', - Medieval: '#2ecc71', - Renaissance: '#9b59b6', - Industrial: '#f1c40f', - Modern: '#e74c3c', - Future: '#95a5a6' - }; - - const nodeWidth = 120; - const nodeHeight = 40; - const xSpacing = 200; - const ySpacing = 80; - const margin = 40; - - const maxLevel = Math.max(...data.map(t => t.level), 0); - const maxEraIndex = Math.max(...data.map(t => eraOrder[t.era] ?? 7), 0); - - const svg = document.getElementById('tech-svg'); - svg.setAttribute('width', margin * 2 + (maxEraIndex + 1) * xSpacing); - svg.setAttribute('height', margin * 2 + (maxLevel + 1) * ySpacing); - - // Arrow head definition - const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); - const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker'); - marker.setAttribute('id', 'arrow'); - marker.setAttribute('viewBox', '0 0 10 10'); - marker.setAttribute('refX', '10'); - marker.setAttribute('refY', '5'); - marker.setAttribute('markerWidth', '6'); - marker.setAttribute('markerHeight', '6'); - marker.setAttribute('orient', 'auto'); - const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - path.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z'); - path.setAttribute('fill', '#999'); - marker.appendChild(path); - defs.appendChild(marker); - svg.appendChild(defs); - - const linkGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - svg.appendChild(linkGroup); - const nodeGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - svg.appendChild(nodeGroup); - - const positions = {}; - data.forEach(t => { - const eraIdx = eraOrder[t.era] ?? 7; - const x = margin + eraIdx * xSpacing; - const y = margin + t.level * ySpacing; - positions[t.id] = { x, y }; + // Sort technologies by era then dependency level + data.sort((a, b) => { + const eraCmp = (eraOrder[a.era] ?? 99) - (eraOrder[b.era] ?? 99); + if (eraCmp !== 0) return eraCmp; + const lvlCmp = a.level - b.level; + if (lvlCmp !== 0) return lvlCmp; + return a.name.localeCompare(b.name); }); - // Draw links + const tbody = document.querySelector('#sorted-table tbody'); data.forEach(t => { - (t.prerequisites || []).forEach(pr => { - const src = positions[pr]; - const dst = positions[t.id]; - if (!src || !dst) return; - const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); - line.setAttribute('class', 'tech-link'); - line.setAttribute('x1', src.x + nodeWidth / 2); - line.setAttribute('y1', src.y + nodeHeight / 2); - line.setAttribute('x2', dst.x + nodeWidth / 2); - line.setAttribute('y2', dst.y + nodeHeight / 2); - line.setAttribute('marker-end', 'url(#arrow)'); - linkGroup.appendChild(line); - }); - }); + const tr = document.createElement('tr'); + const eraTd = document.createElement('td'); + eraTd.textContent = t.era || ''; + tr.appendChild(eraTd); - // Draw nodes - data.forEach(t => { - const pos = positions[t.id]; - const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - g.setAttribute('class', 'tech-node'); - g.setAttribute('transform', `translate(${pos.x},${pos.y})`); + const lvlTd = document.createElement('td'); + lvlTd.textContent = t.level; + tr.appendChild(lvlTd); - const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - rect.setAttribute('width', nodeWidth); - rect.setAttribute('height', nodeHeight); - rect.setAttribute('rx', 6); - rect.setAttribute('fill', eraColors[t.era] || '#cccccc'); - g.appendChild(rect); + const nameTd = document.createElement('td'); + nameTd.textContent = t.name; + tr.appendChild(nameTd); - const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - text.setAttribute('x', nodeWidth / 2); - text.setAttribute('y', nodeHeight / 2); - text.setAttribute('text-anchor', 'middle'); - text.setAttribute('dominant-baseline', 'middle'); - text.textContent = t.name; - g.appendChild(text); + const prereqTd = document.createElement('td'); + prereqTd.textContent = (t.prerequisites || []).join(', '); + tr.appendChild(prereqTd); - nodeGroup.appendChild(g); + tbody.appendChild(tr); }); }); diff --git a/style.css b/style.css index bb8a51b..27198a0 100644 --- a/style.css +++ b/style.css @@ -68,25 +68,22 @@ main { position: relative; } -#tech-svg { +#sorted-table { width: 100%; - height: 100%; + border-collapse: collapse; } -.tech-node rect { - stroke: #2c3e50; - stroke-width: 1; -} - -.tech-node text { - font-size: 12px; - fill: #fff; - pointer-events: none; +#sorted-table th, +#sorted-table td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; } -.tech-link { - stroke: #999; - stroke-width: 1.5; +#sorted-table th { + background-color: #f9f9f9; + position: sticky; + top: 0; } #tech-info-panel {