@@ -5,6 +5,90 @@ import React, { useEffect, useRef } from 'react'; |
| 5 | 5 | import * as d3 from 'd3'; |
| 6 | 6 | import { TreeNode } from '@/lib/api'; |
| 7 | 7 | |
| 8 | +// Quirky visual configuration - moved outside component to avoid dependency issues |
| 9 | +const NODE_CONFIG = { |
| 10 | + sizes: { |
| 11 | + root: { base: 30, hover: 35 }, |
| 12 | + player: { base: 26, hover: 28 }, |
| 13 | + regular: { base: 20, hover: 24 }, |
| 14 | + mole: { base: 22, hover: 26 } |
| 15 | + }, |
| 16 | + colors: { |
| 17 | + player: { |
| 18 | + fill: '#60A5FA', |
| 19 | + stroke: '#3B82F6', |
| 20 | + glow: '#93C5FD' |
| 21 | + }, |
| 22 | + mole: { |
| 23 | + fill: '#F87171', |
| 24 | + stroke: '#DC2626', |
| 25 | + pulse: '#FCA5A5' |
| 26 | + }, |
| 27 | + fhs: { |
| 28 | + fill: '#C084FC', |
| 29 | + stroke: '#9333EA', |
| 30 | + pattern: 'fhs-pattern' |
| 31 | + }, |
| 32 | + regular: { |
| 33 | + fill: '#86EFAC', |
| 34 | + stroke: '#22C55E', |
| 35 | + hover: '#BBF7D0' |
| 36 | + }, |
| 37 | + root: { |
| 38 | + fill: '#FDE047', |
| 39 | + stroke: '#EAB308' |
| 40 | + } |
| 41 | + }, |
| 42 | + strokeWidth: { base: 3, hover: 4 }, |
| 43 | + wobble: { |
| 44 | + amount: 2, |
| 45 | + speed: 3000 |
| 46 | + } |
| 47 | +}; |
| 48 | + |
| 49 | +const ICON_CONFIG = { |
| 50 | + size: 40, |
| 51 | + offset: -20, |
| 52 | + paths: { |
| 53 | + player: '/player.svg', |
| 54 | + mole: '/mole.svg' |
| 55 | + } |
| 56 | +}; |
| 57 | + |
| 58 | +const ANIMATION_CONFIG = { |
| 59 | + nodeHover: { duration: 300 }, |
| 60 | + navigation: { duration: 750, easing: d3.easeCubicInOut }, |
| 61 | + intro: { |
| 62 | + phases: [ |
| 63 | + { duration: 1000 }, |
| 64 | + { duration: 2000, easing: d3.easeCubicInOut }, |
| 65 | + { duration: 1000 }, |
| 66 | + { duration: 1500, easing: d3.easeCubicInOut }, |
| 67 | + { duration: 800 }, |
| 68 | + { duration: 1200, easing: d3.easeCubicInOut }, |
| 69 | + { duration: 1500, easing: d3.easeCubicInOut } |
| 70 | + ] |
| 71 | + }, |
| 72 | + celebration: { duration: '1s', repeatCount: 'indefinite' }, |
| 73 | + pulse: { duration: '2s', repeatCount: 'indefinite' } |
| 74 | +}; |
| 75 | + |
| 76 | +const PARTICLE_CONFIG = { |
| 77 | + count: 30, |
| 78 | + size: { min: 2, max: 6 }, |
| 79 | + colors: ['#FDE047', '#A78BFA', '#F87171', '#60A5FA', '#86EFAC'], |
| 80 | + speed: { min: 20000, max: 40000 } |
| 81 | +}; |
| 82 | + |
| 83 | +const ZOOM_CONFIG = { |
| 84 | + scaleExtent: [0.1, 3] as [number, number], |
| 85 | + defaultScale: 2.5, |
| 86 | + fullTreeScale: 0.8, |
| 87 | + partialTreeScale: 1.5, |
| 88 | + treePadding: 200, |
| 89 | + nudgeOffset: { x: 0.15, y: 0.2 } |
| 90 | +}; |
| 91 | + |
| 8 | 92 | interface TreeVisualizerProps { |
| 9 | 93 | treeData: TreeNode; |
| 10 | 94 | playerLocation: string; |
@@ -26,74 +110,6 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ |
| 26 | 110 | const containerRef = useRef<HTMLDivElement>(null); |
| 27 | 111 | const previousLocationRef = useRef<string | null>(null); |
| 28 | 112 | |
| 29 | | - // Quirky visual configuration |
| 30 | | - const NODE_CONFIG = { |
| 31 | | - sizes: { |
| 32 | | - root: { base: 30, hover: 35 }, |
| 33 | | - player: { base: 26, hover: 28 }, |
| 34 | | - regular: { base: 20, hover: 24 }, |
| 35 | | - mole: { base: 22, hover: 26 } |
| 36 | | - }, |
| 37 | | - colors: { |
| 38 | | - player: { |
| 39 | | - fill: '#60A5FA', |
| 40 | | - stroke: '#3B82F6', |
| 41 | | - glow: '#93C5FD' |
| 42 | | - }, |
| 43 | | - mole: { |
| 44 | | - fill: '#F87171', |
| 45 | | - stroke: '#DC2626', |
| 46 | | - pulse: '#FCA5A5' |
| 47 | | - }, |
| 48 | | - fhs: { |
| 49 | | - fill: '#C084FC', |
| 50 | | - stroke: '#9333EA', |
| 51 | | - pattern: 'fhs-pattern' |
| 52 | | - }, |
| 53 | | - regular: { |
| 54 | | - fill: '#86EFAC', |
| 55 | | - stroke: '#22C55E', |
| 56 | | - hover: '#BBF7D0' |
| 57 | | - }, |
| 58 | | - root: { |
| 59 | | - fill: '#FDE047', |
| 60 | | - stroke: '#EAB308' |
| 61 | | - } |
| 62 | | - }, |
| 63 | | - strokeWidth: { base: 3, hover: 4 }, |
| 64 | | - wobble: { |
| 65 | | - amount: 2, |
| 66 | | - speed: 3000 |
| 67 | | - } |
| 68 | | - }; |
| 69 | | - |
| 70 | | - const ICON_CONFIG = { |
| 71 | | - size: 40, |
| 72 | | - offset: -20, |
| 73 | | - paths: { |
| 74 | | - player: '/player.svg', |
| 75 | | - mole: '/mole.svg' |
| 76 | | - } |
| 77 | | - }; |
| 78 | | - |
| 79 | | - const ANIMATION_CONFIG = { |
| 80 | | - nodeHover: { duration: 300 }, |
| 81 | | - navigation: { duration: 750, easing: d3.easeCubicInOut }, |
| 82 | | - intro: { |
| 83 | | - phases: [ |
| 84 | | - { duration: 1000 }, |
| 85 | | - { duration: 2000, easing: d3.easeCubicInOut }, |
| 86 | | - { duration: 1000 }, |
| 87 | | - { duration: 1500, easing: d3.easeCubicInOut }, |
| 88 | | - { duration: 800 }, |
| 89 | | - { duration: 1200, easing: d3.easeCubicInOut }, |
| 90 | | - { duration: 1500, easing: d3.easeCubicInOut } |
| 91 | | - ] |
| 92 | | - }, |
| 93 | | - celebration: { duration: '1s', repeatCount: 'indefinite' }, |
| 94 | | - pulse: { duration: '2s', repeatCount: 'indefinite' } |
| 95 | | - }; |
| 96 | | - |
| 97 | 113 | const LAYOUT_CONFIG = { |
| 98 | 114 | nodeSpacing: 140, |
| 99 | 115 | margin: { top: 120, right: 160, bottom: 120, left: 160 }, |
@@ -105,15 +121,6 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ |
| 105 | 121 | } |
| 106 | 122 | }; |
| 107 | 123 | |
| 108 | | - const ZOOM_CONFIG = { |
| 109 | | - scaleExtent: [0.1, 3] as [number, number], |
| 110 | | - defaultScale: 2.5, |
| 111 | | - fullTreeScale: 0.8, |
| 112 | | - partialTreeScale: 1.5, |
| 113 | | - treePadding: 200, |
| 114 | | - nudgeOffset: { x: 0.15, y: 0.2 } |
| 115 | | - }; |
| 116 | | - |
| 117 | 124 | const LINK_CONFIG = { |
| 118 | 125 | strokeWidth: 3, |
| 119 | 126 | opacity: 0.6, |
@@ -141,13 +148,6 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ |
| 141 | 148 | } |
| 142 | 149 | }; |
| 143 | 150 | |
| 144 | | - const PARTICLE_CONFIG = { |
| 145 | | - count: 30, |
| 146 | | - size: { min: 2, max: 6 }, |
| 147 | | - colors: ['#FDE047', '#A78BFA', '#F87171', '#60A5FA', '#86EFAC'], |
| 148 | | - speed: { min: 20000, max: 40000 } |
| 149 | | - }; |
| 150 | | - |
| 151 | 151 | const isAnimatingRef = useRef(false); |
| 152 | 152 | |
| 153 | 153 | useEffect(() => { |
@@ -170,7 +170,7 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ |
| 170 | 170 | const root = d3.hierarchy(treeData); |
| 171 | 171 | |
| 172 | 172 | const levelCounts: { [key: number]: number } = {}; |
| 173 | | - root.each(d => { |
| 173 | + root.each((d) => { |
| 174 | 174 | levelCounts[d.depth] = (levelCounts[d.depth] || 0) + 1; |
| 175 | 175 | }); |
| 176 | 176 | const maxNodesAtLevel = Math.max(...Object.values(levelCounts)); |
@@ -284,7 +284,7 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ |
| 284 | 284 | .size([width - margin.left - margin.right, height - margin.top - margin.bottom]) |
| 285 | 285 | .separation((a, b) => { |
| 286 | 286 | const aParentChildCount = a.parent ? (a.parent.children?.length || 0) : 0; |
| 287 | | - const bParentChildCount = b.parent ? (b.parent.children?.length || 0) : 0; |
| 287 | + // const bParentChildCount = b.parent ? (b.parent.children?.length || 0) : 0; |
| 288 | 288 | |
| 289 | 289 | if (a.parent === b.parent && aParentChildCount > 3) { |
| 290 | 290 | const aIsLeaf = !a.children || a.children.length === 0; |
@@ -313,8 +313,8 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ |
| 313 | 313 | |
| 314 | 314 | // Center the root |
| 315 | 315 | const rootX = width / 2; |
| 316 | | - treeNodes.each(d => { |
| 317 | | - d.x = d.x + (rootX - root.x); |
| 316 | + treeNodes.each((d) => { |
| 317 | + d.x = d.x + (rootX - treeNodes.x); |
| 318 | 318 | }); |
| 319 | 319 | |
| 320 | 320 | // Helper function to check if a node is adjacent |
@@ -357,7 +357,7 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ |
| 357 | 357 | .attr('d', linkGenerator) |
| 358 | 358 | .style('fill', 'none') |
| 359 | 359 | .style('stroke', d => { |
| 360 | | - const targetPath = (d.target as any).data.path; |
| 360 | + const targetPath = (d.target as d3.HierarchyPointNode<TreeNode>).data.path; |
| 361 | 361 | if (isAdjacentNode(targetPath, playerLocation) || targetPath === playerLocation) { |
| 362 | 362 | return LINK_CONFIG.colors.adjacent; |
| 363 | 363 | } |
@@ -365,7 +365,7 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ |
| 365 | 365 | }) |
| 366 | 366 | .style('stroke-width', LINK_CONFIG.strokeWidth) |
| 367 | 367 | .style('stroke-dasharray', d => { |
| 368 | | - const targetPath = (d.target as any).data.path; |
| 368 | + const targetPath = (d.target as d3.HierarchyPointNode<TreeNode>).data.path; |
| 369 | 369 | if (targetPath === playerLocation) return 'none'; |
| 370 | 370 | return LINK_CONFIG.dashArray; |
| 371 | 371 | }) |
@@ -493,15 +493,18 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ |
| 493 | 493 | |
| 494 | 494 | // Add interactivity |
| 495 | 495 | node.selectAll('.node-shape') |
| 496 | | - .style('cursor', d => { |
| 496 | + .style('cursor', function(this: any) { |
| 497 | + const d = d3.select(this.parentNode).datum() as d3.HierarchyPointNode<TreeNode>; |
| 497 | 498 | if (d.data.path === playerLocation) return 'default'; |
| 498 | 499 | return isAdjacentNode(d.data.path, playerLocation) ? 'pointer' : 'not-allowed'; |
| 499 | 500 | }) |
| 500 | | - .style('opacity', d => { |
| 501 | + .style('opacity', function(this: any) { |
| 502 | + const d = d3.select(this.parentNode).datum() as d3.HierarchyPointNode<TreeNode>; |
| 501 | 503 | if (d.data.path === playerLocation) return 1; |
| 502 | 504 | return isAdjacentNode(d.data.path, playerLocation) ? 1 : 0.5; |
| 503 | 505 | }) |
| 504 | | - .on('mouseover', function(event, d) { |
| 506 | + .on('mouseover', function(this: any, event: MouseEvent) { |
| 507 | + const d = d3.select(this.parentNode).datum() as d3.HierarchyPointNode<TreeNode>; |
| 505 | 508 | if (d.data.path !== playerLocation && isAdjacentNode(d.data.path, playerLocation)) { |
| 506 | 509 | d3.select(this) |
| 507 | 510 | .transition() |
@@ -513,7 +516,8 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ |
| 513 | 516 | .style('filter', 'url(#glow) drop-shadow(0 0 8px rgba(0,0,0,0.4))'); |
| 514 | 517 | } |
| 515 | 518 | }) |
| 516 | | - .on('mouseout', function(event, d) { |
| 519 | + .on('mouseout', function(this: any, event: MouseEvent) { |
| 520 | + const d = d3.select(this.parentNode).datum() as d3.HierarchyPointNode<TreeNode>; |
| 517 | 521 | d3.select(this) |
| 518 | 522 | .transition() |
| 519 | 523 | .duration(ANIMATION_CONFIG.nodeHover.duration) |
@@ -525,7 +529,8 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ |
| 525 | 529 | }) |
| 526 | 530 | .style('filter', d.data.path === playerLocation ? 'url(#glow)' : 'url(#drop-shadow)'); |
| 527 | 531 | }) |
| 528 | | - .on('click', (event, d) => { |
| 532 | + .on('click', function(this: any, event: MouseEvent) { |
| 533 | + const d = d3.select(this.parentNode).datum() as d3.HierarchyPointNode<TreeNode>; |
| 529 | 534 | if (onNodeClick && d.data.path !== playerLocation && isAdjacentNode(d.data.path, playerLocation)) { |
| 530 | 535 | onNodeClick(d.data.path); |
| 531 | 536 | } |
@@ -808,7 +813,7 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ |
| 808 | 813 | svg.call(zoom.transform, playerTransform); |
| 809 | 814 | } |
| 810 | 815 | } |
| 811 | | - |
| 816 | + // eslint-disable-next-line react-hooks/exhaustive-deps |
| 812 | 817 | }, [treeData, playerLocation, onNodeClick, playIntro, isDarkMode, moleKilled]); |
| 813 | 818 | |
| 814 | 819 | return ( |