zeroed-some/bashamole / ce94102

Browse files

absurdist

Authored by espadonne
SHA
ce94102e82b102e336ae9543a4c86211b6ced415
Parents
eb535ed
Tree
375db63

1 changed file

StatusFile+-
M frontend/src/components/TreeVisualizer.tsx 723 256
frontend/src/components/TreeVisualizer.tsxmodified
1230 lines changed — click to load
@@ -26,21 +26,45 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
2626
   const containerRef = useRef<HTMLDivElement>(null);
2727
   const previousLocationRef = useRef<string | null>(null);
2828
 
29
-  // Visual configuration constants
29
+  // Quirky visual configuration
3030
   const NODE_CONFIG = {
3131
     sizes: {
32
-      root: { base: 26, hover: 26 },
33
-      player: { base: 24, hover: 17 },
34
-      regular: { base: 22, hover: 17 }
32
+      root: { base: 30, hover: 35 },
33
+      player: { base: 26, hover: 28 },
34
+      regular: { base: 20, hover: 24 },
35
+      mole: { base: 22, hover: 26 }
3536
     },
3637
     colors: {
37
-      player: { fill: '#3B82F6', stroke: '#60A5FA' },
38
-      mole: { fill: '#EF4444', stroke: '#F87171' },
39
-      fhs: { fill: '#8B5CF6', stroke: '#ffffff' },
40
-      regular: { fill: '#10B981', stroke: '#ffffff' }
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
+      }
4162
     },
42
-    strokeWidth: { base: 2, hover: 3 },
43
-    glowFilter: 'url(#glow)'
63
+    strokeWidth: { base: 3, hover: 4 },
64
+    wobble: {
65
+      amount: 2,
66
+      speed: 3000
67
+    }
4468
   };
4569
 
4670
   const ICON_CONFIG = {
@@ -53,84 +77,75 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
5377
   };
5478
 
5579
   const ANIMATION_CONFIG = {
56
-    nodeHover: { duration: 200 },
80
+    nodeHover: { duration: 300 },
5781
     navigation: { duration: 750, easing: d3.easeCubicInOut },
5882
     intro: {
5983
       phases: [
60
-        { duration: 1000 }, // Initial pause on root
61
-        { duration: 2000, easing: d3.easeCubicInOut }, // Zoom out to full tree
62
-        { duration: 1000 }, // Pause on full tree
63
-        { duration: 1500, easing: d3.easeCubicInOut }, // Zoom to mole
64
-        { duration: 800 }, // Brief pause on mole
65
-        { duration: 1200, easing: d3.easeCubicInOut }, // Zoom out partially
66
-        { duration: 1500, easing: d3.easeCubicInOut } // Zoom to player
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 }
6791
       ]
6892
     },
69
-    celebration: { duration: '1s', repeatCount: 'indefinite' }
93
+    celebration: { duration: '1s', repeatCount: 'indefinite' },
94
+    pulse: { duration: '2s', repeatCount: 'indefinite' }
7095
   };
7196
 
7297
   const LAYOUT_CONFIG = {
73
-    nodeSpacing: 120,
74
-    margin: { top: 100, right: 150, bottom: 100, left: 150 },
98
+    nodeSpacing: 140,
99
+    margin: { top: 120, right: 160, bottom: 120, left: 160 },
75100
     viewBoxMultiplier: 2.5,
76101
     minHeight: 1200,
77
-    grid: { 
78
-      size: 40, 
79
-      strokeColor: isDarkMode ? '#1f2937' : '#d6d3d1' 
80
-    },
81102
     background: { 
82
-      color: isDarkMode ? '#111827' : '#f5f5f4',
83
-      opacity: isDarkMode ? 0.95 : 1
103
+      color: isDarkMode ? '#0F172A' : '#FEF3C7',
104
+      opacity: 1
84105
     }
85106
   };
86107
 
87108
   const ZOOM_CONFIG = {
88109
     scaleExtent: [0.1, 3] as [number, number],
89
-    defaultScale: 3,
110
+    defaultScale: 2.5,
90111
     fullTreeScale: 0.8,
91
-    partialTreeScale: 1.5, // For the partial zoom out after showing mole
112
+    partialTreeScale: 1.5,
92113
     treePadding: 200,
93114
     nudgeOffset: { x: 0.15, y: 0.2 }
94115
   };
95116
 
96117
   const LINK_CONFIG = {
97
-    gradient: {
98
-      id: 'link-gradient-v',
99
-      start: { 
100
-        color: isDarkMode ? '#4B5563' : '#78716c',
101
-        opacity: isDarkMode ? 0.6 : 0.4
102
-      },
103
-      end: { 
104
-        color: isDarkMode ? '#6B7280' : '#a8a29e',
105
-        opacity: isDarkMode ? 0.3 : 0.2
106
-      }
107
-    },
108
-    strokeWidth: 2,
109
-    opacity: 0.8
118
+    strokeWidth: 3,
119
+    opacity: 0.6,
120
+    dashArray: '5,5',
121
+    colors: {
122
+      default: isDarkMode ? '#475569' : '#92400E',
123
+      hover: isDarkMode ? '#64748B' : '#DC2626',
124
+      adjacent: isDarkMode ? '#3B82F6' : '#2563EB'
125
+    }
110126
   };
111127
 
112128
   const LABEL_CONFIG = {
113
-    fontSize: 14,
114
-    fontWeight: { base: '500', player: '700' },
115
-    offset: { parent: -32, leaf: 40 },
129
+    fontSize: 15,
130
+    fontWeight: { base: '600', player: '800' },
131
+    offset: { parent: -38, leaf: 44 },
116132
     colors: { 
117
-      player: isDarkMode ? '#93C5FD' : '#1e40af',
118
-      regular: isDarkMode ? '#E5E7EB' : '#44403c'
133
+      player: isDarkMode ? '#93C5FD' : '#1E40AF',
134
+      regular: isDarkMode ? '#E5E7EB' : '#451A03',
135
+      mole: '#DC2626'
119136
     },
120
-    textShadow: isDarkMode ? '0 0 4px rgba(0,0,0,0.8)' : '0 0 4px rgba(255,255,255,0.8)'
121
-  };
122
-
123
-  const GLOW_CONFIG = {
124
-    id: 'glow',
125
-    stdDeviation: 3
137
+    background: {
138
+      fill: isDarkMode ? 'rgba(15, 23, 42, 0.9)' : 'rgba(254, 243, 199, 0.9)',
139
+      padding: { x: 8, y: 4 },
140
+      radius: 4
141
+    }
126142
   };
127143
 
128
-  const CELEBRATION_CONFIG = {
129
-    ring: {
130
-      startRadius: 15,
131
-      endRadius: 30,
132
-      strokeWidth: 3
133
-    }
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 }
134149
   };
135150
 
136151
   const isAnimatingRef = useRef(false);
@@ -154,14 +169,12 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
154169
     // Create hierarchy and calculate dimensions
155170
     const root = d3.hierarchy(treeData);
156171
     
157
-    // Calculate the maximum number of nodes at any depth
158172
     const levelCounts: { [key: number]: number } = {};
159173
     root.each(d => {
160174
       levelCounts[d.depth] = (levelCounts[d.depth] || 0) + 1;
161175
     });
162176
     const maxNodesAtLevel = Math.max(...Object.values(levelCounts));
163177
     
164
-    // Dynamic width based on tree structure
165178
     const nodeSpacing = LAYOUT_CONFIG.nodeSpacing;
166179
     const dynamicWidth = Math.max(maxNodesAtLevel * nodeSpacing, containerWidth * LAYOUT_CONFIG.viewBoxMultiplier);
167180
     const margin = LAYOUT_CONFIG.margin;
@@ -175,59 +188,114 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
175188
       .attr('viewBox', `0 0 ${width} ${height}`)
176189
       .attr('preserveAspectRatio', 'xMidYMid meet');
177190
 
178
-    // Add a subtle grid pattern background
191
+    // Add definitions
179192
     const defs = svg.append('defs');
180193
     
181
-    const pattern = defs.append('pattern')
182
-      .attr('id', 'grid')
183
-      .attr('width', LAYOUT_CONFIG.grid.size)
184
-      .attr('height', LAYOUT_CONFIG.grid.size)
185
-      .attr('patternUnits', 'userSpaceOnUse');
194
+    // Create quirky patterns
195
+    const fhsPattern = defs.append('pattern')
196
+      .attr('id', 'fhs-pattern')
197
+      .attr('patternUnits', 'objectBoundingBox')
198
+      .attr('width', 0.25)
199
+      .attr('height', 0.25);
200
+    
201
+    fhsPattern.append('circle')
202
+      .attr('cx', 2)
203
+      .attr('cy', 2)
204
+      .attr('r', 1.5)
205
+      .attr('fill', '#9333EA')
206
+      .attr('opacity', 0.3);
207
+
208
+    // Add glow filters
209
+    const glowFilter = defs.append('filter')
210
+      .attr('id', 'glow');
211
+    
212
+    glowFilter.append('feGaussianBlur')
213
+      .attr('stdDeviation', 4)
214
+      .attr('result', 'coloredBlur');
215
+    
216
+    const feMerge = glowFilter.append('feMerge');
217
+    feMerge.append('feMergeNode').attr('in', 'coloredBlur');
218
+    feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
219
+
220
+    // Add drop shadow filter
221
+    const dropShadow = defs.append('filter')
222
+      .attr('id', 'drop-shadow')
223
+      .attr('x', '-50%')
224
+      .attr('y', '-50%')
225
+      .attr('width', '200%')
226
+      .attr('height', '200%');
227
+    
228
+    dropShadow.append('feGaussianBlur')
229
+      .attr('in', 'SourceAlpha')
230
+      .attr('stdDeviation', 3);
186231
     
187
-    pattern.append('path')
188
-      .attr('d', `M ${LAYOUT_CONFIG.grid.size} 0 L 0 0 0 ${LAYOUT_CONFIG.grid.size}`)
189
-      .attr('fill', 'none')
190
-      .attr('stroke', LAYOUT_CONFIG.grid.strokeColor)
191
-      .attr('stroke-width', '1');
232
+    dropShadow.append('feOffset')
233
+      .attr('dx', 2)
234
+      .attr('dy', 2)
235
+      .attr('result', 'offsetblur');
236
+    
237
+    const feMerge2 = dropShadow.append('feMerge');
238
+    feMerge2.append('feMergeNode').attr('in', 'offsetblur');
239
+    feMerge2.append('feMergeNode').attr('in', 'SourceGraphic');
192240
 
241
+    // Quirky background with floating particles
193242
     svg.append('rect')
194243
       .attr('width', '100%')
195244
       .attr('height', '100%')
196245
       .attr('fill', LAYOUT_CONFIG.background.color)
197246
       .style('opacity', LAYOUT_CONFIG.background.opacity);
247
+
248
+    // Add floating background particles
249
+    const particlesGroup = svg.append('g').attr('class', 'particles');
198250
     
199
-    svg.append('rect')
200
-      .attr('width', '100%')
201
-      .attr('height', '100%')
202
-      .attr('fill', 'url(#grid)')
203
-      .style('opacity', isDarkMode ? 0.3 : 0.1);
251
+    for (let i = 0; i < PARTICLE_CONFIG.count; i++) {
252
+      const particle = particlesGroup.append('circle')
253
+        .attr('cx', Math.random() * width)
254
+        .attr('cy', Math.random() * height)
255
+        .attr('r', Math.random() * (PARTICLE_CONFIG.size.max - PARTICLE_CONFIG.size.min) + PARTICLE_CONFIG.size.min)
256
+        .attr('fill', PARTICLE_CONFIG.colors[Math.floor(Math.random() * PARTICLE_CONFIG.colors.length)])
257
+        .attr('opacity', 0.3);
258
+
259
+      // Animate particles floating
260
+      particle
261
+        .transition()
262
+        .duration(Math.random() * (PARTICLE_CONFIG.speed.max - PARTICLE_CONFIG.speed.min) + PARTICLE_CONFIG.speed.min)
263
+        .ease(d3.easeLinear)
264
+        .attr('cy', -20)
265
+        .on('end', function repeat() {
266
+          d3.select(this)
267
+            .attr('cy', height + 20)
268
+            .attr('cx', Math.random() * width)
269
+            .transition()
270
+            .duration(Math.random() * (PARTICLE_CONFIG.speed.max - PARTICLE_CONFIG.speed.min) + PARTICLE_CONFIG.speed.min)
271
+            .ease(d3.easeLinear)
272
+            .attr('cy', -20)
273
+            .on('end', repeat);
274
+        });
275
+    }
204276
 
205277
     const g = svg
206278
       .append('g')
207279
       .attr('transform', `translate(${margin.left},${margin.top})`);
208280
 
209
-    // Create tree layout - vertical orientation with better spacing
281
+    // Create tree layout
210282
     const treeLayout = d3
211283
       .tree<TreeNode>()
212284
       .size([width - margin.left - margin.right, height - margin.top - margin.bottom])
213285
       .separation((a, b) => {
214
-        // Special handling for directories with many children
215286
         const aParentChildCount = a.parent ? (a.parent.children?.length || 0) : 0;
216287
         const bParentChildCount = b.parent ? (b.parent.children?.length || 0) : 0;
217288
         
218
-        // If nodes share a parent with many children (like home dirs), give more space
219289
         if (a.parent === b.parent && aParentChildCount > 3) {
220290
           const aIsLeaf = !a.children || a.children.length === 0;
221291
           const bIsLeaf = !b.children || b.children.length === 0;
222292
           
223293
           if (aIsLeaf && bIsLeaf) {
224
-            // Extra space for leaf nodes in crowded directories
225294
             return 2.5;
226295
           }
227296
           return 2;
228297
         }
229298
         
230
-        // Base separation on node depth
231299
         if (a.depth === 0 || b.depth === 0) return 4;
232300
         if (a.depth === 1 || b.depth === 1) return 3;
233301
         
@@ -243,46 +311,83 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
243311
     // Apply tree layout
244312
     const treeNodes = treeLayout(root);
245313
     
246
-    // Adjust the x-coordinate to center the root
314
+    // Center the root
247315
     const rootX = width / 2;
248316
     treeNodes.each(d => {
249317
       d.x = d.x + (rootX - root.x);
250318
     });
251319
 
252
-    // Create gradient for links
253
-    const linkGradient = defs
254
-      .append('linearGradient')
255
-      .attr('id', LINK_CONFIG.gradient.id)
256
-      .attr('gradientUnits', 'userSpaceOnUse')
257
-      .attr('x1', '0%')
258
-      .attr('y1', '0%')
259
-      .attr('x2', '0%')
260
-      .attr('y2', '100%');
261
-    
262
-    linkGradient.append('stop')
263
-      .attr('offset', '0%')
264
-      .attr('stop-color', LINK_CONFIG.gradient.start.color)
265
-      .attr('stop-opacity', LINK_CONFIG.gradient.start.opacity);
266
-    
267
-    linkGradient.append('stop')
268
-      .attr('offset', '100%')
269
-      .attr('stop-color', LINK_CONFIG.gradient.end.color)
270
-      .attr('stop-opacity', LINK_CONFIG.gradient.end.opacity);
320
+    // Helper function to check if a node is adjacent
321
+    const isAdjacentNode = (nodePath: string, currentPath: string): boolean => {
322
+      if (currentPath !== '/') {
323
+        const parentPath = currentPath.substring(0, currentPath.lastIndexOf('/')) || '/';
324
+        if (nodePath === parentPath) return true;
325
+      }
326
+      
327
+      if (currentPath === '/') {
328
+        const segments = nodePath.split('/').filter(s => s);
329
+        if (segments.length === 1) return true;
330
+      } else {
331
+        if (nodePath.startsWith(currentPath + '/')) {
332
+          const relativePath = nodePath.substring(currentPath.length + 1);
333
+          if (!relativePath.includes('/')) return true;
334
+        }
335
+      }
336
+      
337
+      return false;
338
+    };
339
+
340
+    // Create quirky curved links
341
+    const linkGenerator = d3.linkVertical<any, any>()
342
+      .x(d => d.x)
343
+      .y(d => d.y)
344
+      .source(d => {
345
+        // Add some wobble to the source point
346
+        const wobbleX = Math.sin(Date.now() / NODE_CONFIG.wobble.speed) * NODE_CONFIG.wobble.amount;
347
+        return { x: d.source.x + wobbleX, y: d.source.y };
348
+      })
349
+      .target(d => d.target);
271350
 
272
-    // Create links with vertical layout
273351
     const link = g
274352
       .selectAll('.link')
275353
       .data(treeNodes.links())
276354
       .enter()
277355
       .append('path')
278356
       .attr('class', 'link')
279
-      .attr('d', d3.linkVertical<any, any>()
280
-        .x(d => d.x)
281
-        .y(d => d.y))
357
+      .attr('d', linkGenerator)
282358
       .style('fill', 'none')
283
-      .style('stroke', `url(#${LINK_CONFIG.gradient.id})`)
359
+      .style('stroke', d => {
360
+        const targetPath = (d.target as any).data.path;
361
+        if (isAdjacentNode(targetPath, playerLocation) || targetPath === playerLocation) {
362
+          return LINK_CONFIG.colors.adjacent;
363
+        }
364
+        return LINK_CONFIG.colors.default;
365
+      })
284366
       .style('stroke-width', LINK_CONFIG.strokeWidth)
285
-      .style('opacity', LINK_CONFIG.opacity);
367
+      .style('stroke-dasharray', d => {
368
+        const targetPath = (d.target as any).data.path;
369
+        if (targetPath === playerLocation) return 'none';
370
+        return LINK_CONFIG.dashArray;
371
+      })
372
+      .style('opacity', LINK_CONFIG.opacity)
373
+      .style('filter', 'drop-shadow(0 0 3px rgba(0,0,0,0.3))');
374
+
375
+    // Animate link dashes
376
+    link
377
+      .style('stroke-dashoffset', 0)
378
+      .transition()
379
+      .duration(20000)
380
+      .ease(d3.easeLinear)
381
+      .style('stroke-dashoffset', -100)
382
+      .on('end', function repeat() {
383
+        d3.select(this)
384
+          .style('stroke-dashoffset', 0)
385
+          .transition()
386
+          .duration(20000)
387
+          .ease(d3.easeLinear)
388
+          .style('stroke-dashoffset', -100)
389
+          .on('end', repeat);
390
+      });
286391
 
287392
     // Create node groups
288393
     const node = g
@@ -293,152 +398,527 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
293398
       .attr('class', 'node')
294399
       .attr('transform', d => `translate(${d.x},${d.y})`);
295400
 
296
-    // Add glow effect for interactive nodes
297
-    const glowFilter = defs.append('filter')
298
-      .attr('id', GLOW_CONFIG.id);
299
-    
300
-    glowFilter.append('feGaussianBlur')
301
-      .attr('stdDeviation', GLOW_CONFIG.stdDeviation)
302
-      .attr('result', 'coloredBlur');
303
-    
304
-    const feMerge = glowFilter.append('feMerge');
305
-    feMerge.append('feMergeNode').attr('in', 'coloredBlur');
306
-    feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
307
-
308
-    // Add a subtle pulse animation for adjacent nodes
309
-    const pulseAnimation = defs.append('style')
310
-      .text(`
311
-        @keyframes subtlePulse {
312
-          0%, 100% { opacity: 1; }
313
-          50% { opacity: 0.8; }
314
-        }
315
-        .adjacent-node {
316
-          animation: subtlePulse 2s ease-in-out infinite;
317
-        }
318
-      `);
401
+    // Add subtle wobble animation to all nodes
402
+    node.each(function(d, i) {
403
+      const nodeGroup = d3.select(this);
404
+      const delay = i * 100;
405
+      
406
+      nodeGroup
407
+        .transition()
408
+        .delay(delay)
409
+        .duration(NODE_CONFIG.wobble.speed)
410
+        .ease(d3.easeSinInOut)
411
+        .attr('transform', `translate(${d.x + NODE_CONFIG.wobble.amount},${d.y})`)
412
+        .transition()
413
+        .duration(NODE_CONFIG.wobble.speed)
414
+        .ease(d3.easeSinInOut)
415
+        .attr('transform', `translate(${d.x - NODE_CONFIG.wobble.amount},${d.y})`)
416
+        .on('end', function repeat() {
417
+          d3.select(this)
418
+            .transition()
419
+            .duration(NODE_CONFIG.wobble.speed)
420
+            .ease(d3.easeSinInOut)
421
+            .attr('transform', `translate(${d.x + NODE_CONFIG.wobble.amount},${d.y})`)
422
+            .transition()
423
+            .duration(NODE_CONFIG.wobble.speed)
424
+            .ease(d3.easeSinInOut)
425
+            .attr('transform', `translate(${d.x - NODE_CONFIG.wobble.amount},${d.y})`)
426
+            .on('end', repeat);
427
+        });
428
+    });
319429
 
320
-    // Helper function to check if a node is adjacent to the current location
321
-    const isAdjacentNode = (nodePath: string, currentPath: string): boolean => {
322
-      // Check if it's the parent directory
323
-      if (currentPath !== '/') {
324
-        const parentPath = currentPath.substring(0, currentPath.lastIndexOf('/')) || '/';
325
-        if (nodePath === parentPath) return true;
326
-      }
430
+    // Add node backgrounds (quirky shapes)
431
+    node.each(function(d) {
432
+      const nodeEl = d3.select(this);
433
+      const isRoot = d.data.path === '/';
434
+      const isPlayer = d.data.path === playerLocation;
435
+      const hasMole = d.data.has_mole;
327436
       
328
-      // Check if it's a direct child
329
-      if (currentPath === '/') {
330
-        // For root, children are paths with exactly one segment
331
-        const segments = nodePath.split('/').filter(s => s);
332
-        if (segments.length === 1) return true;
333
-      } else {
334
-        // For other directories, check if it's a direct child
335
-        if (nodePath.startsWith(currentPath + '/')) {
336
-          const relativePath = nodePath.substring(currentPath.length + 1);
337
-          // Make sure there are no additional slashes (not a grandchild)
338
-          if (!relativePath.includes('/')) return true;
437
+      if (isRoot) {
438
+        // Star shape for root
439
+        const starPoints = 8;
440
+        const outerRadius = NODE_CONFIG.sizes.root.base;
441
+        const innerRadius = outerRadius * 0.6;
442
+        
443
+        let path = '';
444
+        for (let i = 0; i < starPoints * 2; i++) {
445
+          const angle = (i * Math.PI) / starPoints;
446
+          const radius = i % 2 === 0 ? outerRadius : innerRadius;
447
+          const x = Math.cos(angle) * radius;
448
+          const y = Math.sin(angle) * radius;
449
+          path += `${i === 0 ? 'M' : 'L'} ${x},${y}`;
339450
         }
451
+        path += 'Z';
452
+        
453
+        nodeEl.append('path')
454
+          .attr('d', path)
455
+          .attr('class', 'node-shape')
456
+          .style('fill', NODE_CONFIG.colors.root.fill)
457
+          .style('stroke', NODE_CONFIG.colors.root.stroke)
458
+          .style('stroke-width', NODE_CONFIG.strokeWidth.base)
459
+          .style('filter', 'url(#drop-shadow)');
460
+      } else if (hasMole) {
461
+        // Irregular shape for mole locations
462
+        const size = NODE_CONFIG.sizes.mole.base;
463
+        nodeEl.append('path')
464
+          .attr('d', `M ${-size},0 Q ${-size/2},${-size} 0,${-size} T ${size},0 Q ${size/2},${size} 0,${size} T ${-size},0`)
465
+          .attr('class', 'node-shape mole-node')
466
+          .style('fill', NODE_CONFIG.colors.mole.fill)
467
+          .style('stroke', NODE_CONFIG.colors.mole.stroke)
468
+          .style('stroke-width', NODE_CONFIG.strokeWidth.base)
469
+          .style('filter', 'url(#glow)');
470
+      } else {
471
+        // Regular circles with personality
472
+        nodeEl.append('circle')
473
+          .attr('r', () => {
474
+            if (isPlayer) return NODE_CONFIG.sizes.player.base;
475
+            if (d.data.is_fhs) return NODE_CONFIG.sizes.regular.base + 2;
476
+            return NODE_CONFIG.sizes.regular.base;
477
+          })
478
+          .attr('class', 'node-shape')
479
+          .style('fill', () => {
480
+            if (isPlayer) return NODE_CONFIG.colors.player.fill;
481
+            if (d.data.is_fhs) return `url(#${NODE_CONFIG.colors.fhs.pattern})`;
482
+            return NODE_CONFIG.colors.regular.fill;
483
+          })
484
+          .style('stroke', () => {
485
+            if (isPlayer) return NODE_CONFIG.colors.player.stroke;
486
+            if (d.data.is_fhs) return NODE_CONFIG.colors.fhs.stroke;
487
+            return NODE_CONFIG.colors.regular.stroke;
488
+          })
489
+          .style('stroke-width', NODE_CONFIG.strokeWidth.base)
490
+          .style('filter', isPlayer ? 'url(#glow)' : 'url(#drop-shadow)');
340491
       }
341
-      
342
-      return false;
343
-    };
492
+    });
344493
 
345
-    // Add circles for nodes
346
-    node
347
-      .append('circle')
348
-      .attr('r', d => {
349
-        if (d.data.path === '/') return NODE_CONFIG.sizes.root.base;
350
-        if (d.data.path === playerLocation) return NODE_CONFIG.sizes.player.base;
351
-        return NODE_CONFIG.sizes.regular.base;
352
-      })
353
-      .style('fill', d => {
354
-        if (d.data.path === playerLocation) return NODE_CONFIG.colors.player.fill;
355
-        if (d.data.has_mole) return NODE_CONFIG.colors.mole.fill;
356
-        if (d.data.is_fhs) return NODE_CONFIG.colors.fhs.fill;
357
-        return NODE_CONFIG.colors.regular.fill;
358
-      })
359
-      .style('stroke', d => {
360
-        if (d.data.path === playerLocation) return NODE_CONFIG.colors.player.stroke;
361
-        if (d.data.has_mole) return NODE_CONFIG.colors.mole.stroke;
362
-        return NODE_CONFIG.colors.regular.stroke;
363
-      })
364
-      .style('stroke-width', NODE_CONFIG.strokeWidth.base)
494
+    // Add interactivity
495
+    node.selectAll('.node-shape')
365496
       .style('cursor', d => {
366
-        // Only show pointer cursor for adjacent nodes
367497
         if (d.data.path === playerLocation) return 'default';
368498
         return isAdjacentNode(d.data.path, playerLocation) ? 'pointer' : 'not-allowed';
369499
       })
370
-      .style('filter', d => d.data.path === playerLocation ? NODE_CONFIG.glowFilter : 'none')
371500
       .style('opacity', d => {
372
-        // Slightly fade non-adjacent nodes
373501
         if (d.data.path === playerLocation) return 1;
374
-        return isAdjacentNode(d.data.path, playerLocation) ? 1 : 0.6;
375
-      })
376
-      .style('transition', 'all 0.3s ease')
377
-      .attr('class', d => {
378
-        // Add class for adjacent nodes to enable pulse animation
379
-        if (d.data.path !== playerLocation && isAdjacentNode(d.data.path, playerLocation)) {
380
-          return 'adjacent-node';
381
-        }
382
-        return '';
502
+        return isAdjacentNode(d.data.path, playerLocation) ? 1 : 0.5;
383503
       })
384504
       .on('mouseover', function(event, d) {
385
-        // Only apply hover effect to adjacent nodes
386505
         if (d.data.path !== playerLocation && isAdjacentNode(d.data.path, playerLocation)) {
387506
           d3.select(this)
388507
             .transition()
389508
             .duration(ANIMATION_CONFIG.nodeHover.duration)
390
-            .attr('r', d.data.path === '/' ? NODE_CONFIG.sizes.root.hover : NODE_CONFIG.sizes.regular.hover)
391
-            .style('stroke-width', NODE_CONFIG.strokeWidth.hover);
509
+            .attr('r', function() {
510
+              const currentR = d3.select(this).attr('r');
511
+              return currentR ? parseFloat(currentR) * 1.2 : NODE_CONFIG.sizes.regular.hover;
512
+            })
513
+            .style('filter', 'url(#glow) drop-shadow(0 0 8px rgba(0,0,0,0.4))');
392514
         }
393515
       })
394516
       .on('mouseout', function(event, d) {
395517
         d3.select(this)
396518
           .transition()
397519
           .duration(ANIMATION_CONFIG.nodeHover.duration)
398
-          .attr('r', d.data.path === '/' ? NODE_CONFIG.sizes.root.base : 
399
-                    d.data.path === playerLocation ? NODE_CONFIG.sizes.player.base : 
400
-                    NODE_CONFIG.sizes.regular.base)
401
-          .style('stroke-width', NODE_CONFIG.strokeWidth.base);
520
+          .attr('r', function() {
521
+            if (d.data.path === '/') return NODE_CONFIG.sizes.root.base;
522
+            if (d.data.path === playerLocation) return NODE_CONFIG.sizes.player.base;
523
+            if (d.data.has_mole) return NODE_CONFIG.sizes.mole.base;
524
+            return NODE_CONFIG.sizes.regular.base;
525
+          })
526
+          .style('filter', d.data.path === playerLocation ? 'url(#glow)' : 'url(#drop-shadow)');
402527
       })
403528
       .on('click', (event, d) => {
404
-        // Only allow clicks on adjacent nodes
405529
         if (onNodeClick && d.data.path !== playerLocation && isAdjacentNode(d.data.path, playerLocation)) {
406530
           onNodeClick(d.data.path);
407531
         }
408532
       });
409533
 
410
-    // Add tooltips
411
-    node
412
-      .append('title')
413
-      .text(d => {
414
-        const baseText = `${d.data.path}\n${d.data.description}`;
415
-        const moleText = d.data.has_mole ? '\n🐭 Mole is here!' : '';
416
-        const clickableText = (d.data.path !== playerLocation && isAdjacentNode(d.data.path, playerLocation)) 
417
-          ? '\n(Click to navigate here)' 
418
-          : '';
419
-        return baseText + moleText + clickableText;
420
-      });
534
+    // Add pulse animation to mole nodes
535
+    node.filter(d => d.data.has_mole)
536
+      .select('.mole-node')
537
+      .append('animate')
538
+      .attr('attributeName', 'opacity')
539
+      .attr('values', '0.7;1;0.7')
540
+      .attr('dur', ANIMATION_CONFIG.pulse.duration)
541
+      .attr('repeatCount', ANIMATION_CONFIG.pulse.repeatCount);
542
+
543
+    // Add labels with backgrounds
544
+    const labels = node
545
+      .append('g')
546
+      .attr('class', 'label-group');
421547
 
422
-    // Add labels
423
-    node
424
-      .append('text')
548
+    // Label background
549
+    labels.append('rect')
550
+      .attr('class', 'label-bg')
551
+      .attr('fill', LABEL_CONFIG.background.fill)
552
+      .attr('rx', LABEL_CONFIG.background.radius)
553
+      .attr('ry', LABEL_CONFIG.background.radius)
554
+      .style('filter', 'drop-shadow(0 1px 2px rgba(0,0,0,0.2))');
555
+
556
+    // Label text
557
+    labels.append('text')
558
+      .attr('class', 'label-text')
425559
       .attr('dy', d => d.children ? LABEL_CONFIG.offset.parent : LABEL_CONFIG.offset.leaf)
426560
       .attr('text-anchor', 'middle')
427561
       .style('font-size', `${LABEL_CONFIG.fontSize}px`)
428562
       .style('font-weight', d => d.data.path === playerLocation ? LABEL_CONFIG.fontWeight.player : LABEL_CONFIG.fontWeight.base)
429
-      .style('fill', d => d.data.path === playerLocation ? LABEL_CONFIG.colors.player : LABEL_CONFIG.colors.regular)
430
-      .style('text-shadow', LABEL_CONFIG.textShadow)
563
+      .style('fill', d => {
564
+        if (d.data.path === playerLocation) return LABEL_CONFIG.colors.player;
565
+        if (d.data.has_mole) return LABEL_CONFIG.colors.mole;
566
+        return LABEL_CONFIG.colors.regular;
567
+      })
568
+      .style('font-family', 'Comic Sans MS, cursive')
431569
       .text(d => d.data.name || '/')
432570
       .style('pointer-events', 'none');
433571
 
434
-    // Add player indicator with SVG overlaid on node
572
+    // Size backgrounds to fit text
573
+    labels.each(function() {
574
+      const labelGroup = d3.select(this);
575
+      const text = labelGroup.select('.label-text');
576
+      const bg = labelGroup.select('.label-bg');
577
+      
578
+      const bbox = (text.node() as SVGTextElement).getBBox();
579
+      
580
+      bg.attr('x', bbox.x - LABEL_CONFIG.background.padding.x)
581
+        .attr('y', bbox.y - LABEL_CONFIG.background.padding.y)
582
+        .attr('width', bbox.width + LABEL_CONFIG.background.padding.x * 2)
583
+        .attr('height', bbox.height + LABEL_CONFIG.background.padding.y * 2);
584
+    });
585
+
586
+    // Add minimal SVG icons for special directories
587
+    node.each(function(d) {
588
+      const nodeEl = d3.select(this);
589
+      const iconSize = 24;
590
+      const iconOffset = -iconSize / 2;
591
+      
592
+      // Create icon group
593
+      const iconGroup = nodeEl.append('g')
594
+        .attr('class', 'directory-icon')
595
+        .style('pointer-events', 'none');
596
+      
597
+      if (d.data.path === '/home') {
598
+        // Simple house icon
599
+        iconGroup.append('path')
600
+          .attr('d', `M ${iconOffset} 5 L 0 ${iconOffset + 5} L ${iconSize/2} 5 L ${iconSize/2} ${iconSize/2 - 2} L ${iconOffset} ${iconSize/2 - 2} Z`)
601
+          .attr('fill', '#F59E0B')
602
+          .attr('stroke', '#92400E')
603
+          .attr('stroke-width', 1.5);
604
+        
605
+        iconGroup.append('rect')
606
+          .attr('x', iconOffset + 8)
607
+          .attr('y', 5)
608
+          .attr('width', 8)
609
+          .attr('height', 10)
610
+          .attr('fill', '#92400E');
611
+          
612
+      } else if (d.data.path === '/tmp') {
613
+        // Simple trash can icon
614
+        iconGroup.append('rect')
615
+          .attr('x', iconOffset + 4)
616
+          .attr('y', iconOffset + 8)
617
+          .attr('width', 16)
618
+          .attr('height', 14)
619
+          .attr('rx', 1)
620
+          .attr('fill', '#6B7280')
621
+          .attr('stroke', '#374151')
622
+          .attr('stroke-width', 1.5);
623
+        
624
+        iconGroup.append('rect')
625
+          .attr('x', iconOffset + 2)
626
+          .attr('y', iconOffset + 6)
627
+          .attr('width', 20)
628
+          .attr('height', 3)
629
+          .attr('fill', '#374151');
630
+        
631
+        iconGroup.append('line')
632
+          .attr('x1', iconOffset + 8)
633
+          .attr('y1', iconOffset + 12)
634
+          .attr('x2', iconOffset + 8)
635
+          .attr('y2', iconOffset + 18)
636
+          .attr('stroke', '#9CA3AF')
637
+          .attr('stroke-width', 1);
638
+        
639
+        iconGroup.append('line')
640
+          .attr('x1', iconOffset + 12)
641
+          .attr('y1', iconOffset + 12)
642
+          .attr('x2', iconOffset + 12)
643
+          .attr('y2', iconOffset + 18)
644
+          .attr('stroke', '#9CA3AF')
645
+          .attr('stroke-width', 1);
646
+        
647
+        iconGroup.append('line')
648
+          .attr('x1', iconOffset + 16)
649
+          .attr('y1', iconOffset + 12)
650
+          .attr('x2', iconOffset + 16)
651
+          .attr('y2', iconOffset + 18)
652
+          .attr('stroke', '#9CA3AF')
653
+          .attr('stroke-width', 1);
654
+          
655
+      } else if (d.data.path === '/etc') {
656
+        // Simple gear/config icon
657
+        iconGroup.append('circle')
658
+          .attr('cx', iconOffset + 12)
659
+          .attr('cy', iconOffset + 12)
660
+          .attr('r', 8)
661
+          .attr('fill', '#8B5CF6')
662
+          .attr('stroke', '#6D28D9')
663
+          .attr('stroke-width', 1.5);
664
+        
665
+        iconGroup.append('circle')
666
+          .attr('cx', iconOffset + 12)
667
+          .attr('cy', iconOffset + 12)
668
+          .attr('r', 4)
669
+          .attr('fill', '#DDD6FE');
670
+        
671
+        // Gear teeth
672
+        for (let i = 0; i < 8; i++) {
673
+          const angle = (i * Math.PI * 2) / 8;
674
+          const x1 = iconOffset + 12 + Math.cos(angle) * 6;
675
+          const y1 = iconOffset + 12 + Math.sin(angle) * 6;
676
+          const x2 = iconOffset + 12 + Math.cos(angle) * 9;
677
+          const y2 = iconOffset + 12 + Math.sin(angle) * 9;
678
+          
679
+          iconGroup.append('line')
680
+            .attr('x1', x1)
681
+            .attr('y1', y1)
682
+            .attr('x2', x2)
683
+            .attr('y2', y2)
684
+            .attr('stroke', '#6D28D9')
685
+            .attr('stroke-width', 2)
686
+            .attr('stroke-linecap', 'round');
687
+        }
688
+        
689
+      } else if (d.data.path === '/bin' || d.data.path === '/sbin') {
690
+        // Simple terminal/binary icon
691
+        iconGroup.append('rect')
692
+          .attr('x', iconOffset + 4)
693
+          .attr('y', iconOffset + 6)
694
+          .attr('width', 16)
695
+          .attr('height', 12)
696
+          .attr('rx', 2)
697
+          .attr('fill', '#1F2937')
698
+          .attr('stroke', '#374151')
699
+          .attr('stroke-width', 1.5);
700
+        
701
+        iconGroup.append('text')
702
+          .attr('x', iconOffset + 12)
703
+          .attr('y', iconOffset + 14)
704
+          .attr('text-anchor', 'middle')
705
+          .attr('fill', '#10B981')
706
+          .attr('font-family', 'monospace')
707
+          .attr('font-size', '10px')
708
+          .attr('font-weight', 'bold')
709
+          .text('>_');
710
+          
711
+      } else if (d.data.path === '/var') {
712
+        // Simple database/variable icon
713
+        iconGroup.append('ellipse')
714
+          .attr('cx', iconOffset + 12)
715
+          .attr('cy', iconOffset + 8)
716
+          .attr('rx', 8)
717
+          .attr('ry', 3)
718
+          .attr('fill', '#F59E0B')
719
+          .attr('stroke', '#D97706')
720
+          .attr('stroke-width', 1.5);
721
+        
722
+        iconGroup.append('rect')
723
+          .attr('x', iconOffset + 4)
724
+          .attr('y', iconOffset + 8)
725
+          .attr('width', 16)
726
+          .attr('height', 8)
727
+          .attr('fill', '#F59E0B');
728
+        
729
+        iconGroup.append('ellipse')
730
+          .attr('cx', iconOffset + 12)
731
+          .attr('cy', iconOffset + 16)
732
+          .attr('rx', 8)
733
+          .attr('ry', 3)
734
+          .attr('fill', '#FBBF24')
735
+          .attr('stroke', '#D97706')
736
+          .attr('stroke-width', 1.5);
737
+          
738
+      } else if (d.data.path === '/usr') {
739
+        // Simple user/folder icon
740
+        iconGroup.append('path')
741
+          .attr('d', `M ${iconOffset + 4} ${iconOffset + 8} L ${iconOffset + 4} ${iconOffset + 18} L ${iconOffset + 20} ${iconOffset + 18} L ${iconOffset + 20} ${iconOffset + 10} L ${iconOffset + 18} ${iconOffset + 8} Z`)
742
+          .attr('fill', '#3B82F6')
743
+          .attr('stroke', '#2563EB')
744
+          .attr('stroke-width', 1.5);
745
+        
746
+        iconGroup.append('path')
747
+          .attr('d', `M ${iconOffset + 4} ${iconOffset + 8} L ${iconOffset + 10} ${iconOffset + 8} L ${iconOffset + 12} ${iconOffset + 10} L ${iconOffset + 18} ${iconOffset + 10}`)
748
+          .attr('fill', 'none')
749
+          .attr('stroke', '#2563EB')
750
+          .attr('stroke-width', 1.5);
751
+          
752
+      } else if (d.data.path === '/opt') {
753
+        // Simple package/box icon
754
+        iconGroup.append('path')
755
+          .attr('d', `M ${iconOffset + 4} ${iconOffset + 10} L ${iconOffset + 12} ${iconOffset + 6} L ${iconOffset + 20} ${iconOffset + 10} L ${iconOffset + 20} ${iconOffset + 18} L ${iconOffset + 12} ${iconOffset + 22} L ${iconOffset + 4} ${iconOffset + 18} Z`)
756
+          .attr('fill', '#10B981')
757
+          .attr('stroke', '#059669')
758
+          .attr('stroke-width', 1.5);
759
+        
760
+        iconGroup.append('path')
761
+          .attr('d', `M ${iconOffset + 4} ${iconOffset + 10} L ${iconOffset + 12} ${iconOffset + 14} L ${iconOffset + 20} ${iconOffset + 10}`)
762
+          .attr('fill', 'none')
763
+          .attr('stroke', '#059669')
764
+          .attr('stroke-width', 1.5);
765
+        
766
+        iconGroup.append('line')
767
+          .attr('x1', iconOffset + 12)
768
+          .attr('y1', iconOffset + 14)
769
+          .attr('x2', iconOffset + 12)
770
+          .attr('y2', iconOffset + 22)
771
+          .attr('stroke', '#059669')
772
+          .attr('stroke-width', 1.5);
773
+          
774
+      } else if (d.data.path.includes('Documents')) {
775
+        // Simple document icon
776
+        iconGroup.append('rect')
777
+          .attr('x', iconOffset + 6)
778
+          .attr('y', iconOffset + 4)
779
+          .attr('width', 12)
780
+          .attr('height', 16)
781
+          .attr('rx', 1)
782
+          .attr('fill', '#E5E7EB')
783
+          .attr('stroke', '#6B7280')
784
+          .attr('stroke-width', 1.5);
785
+        
786
+        iconGroup.append('path')
787
+          .attr('d', `M ${iconOffset + 12} ${iconOffset + 4} L ${iconOffset + 18} ${iconOffset + 4} L ${iconOffset + 18} ${iconOffset + 10}`)
788
+          .attr('fill', '#D1D5DB')
789
+          .attr('stroke', '#6B7280')
790
+          .attr('stroke-width', 1.5);
791
+        
792
+        // Text lines
793
+        iconGroup.append('line')
794
+          .attr('x1', iconOffset + 9)
795
+          .attr('y1', iconOffset + 12)
796
+          .attr('x2', iconOffset + 15)
797
+          .attr('y2', iconOffset + 12)
798
+          .attr('stroke', '#6B7280')
799
+          .attr('stroke-width', 1);
800
+        
801
+        iconGroup.append('line')
802
+          .attr('x1', iconOffset + 9)
803
+          .attr('y1', iconOffset + 15)
804
+          .attr('x2', iconOffset + 15)
805
+          .attr('y2', iconOffset + 15)
806
+          .attr('stroke', '#6B7280')
807
+          .attr('stroke-width', 1);
808
+          
809
+      } else if (d.data.path.includes('Pictures')) {
810
+        // Simple picture frame icon
811
+        iconGroup.append('rect')
812
+          .attr('x', iconOffset + 4)
813
+          .attr('y', iconOffset + 6)
814
+          .attr('width', 16)
815
+          .attr('height', 12)
816
+          .attr('rx', 1)
817
+          .attr('fill', '#DBEAFE')
818
+          .attr('stroke', '#3B82F6')
819
+          .attr('stroke-width', 1.5);
820
+        
821
+        // Mountain
822
+        iconGroup.append('path')
823
+          .attr('d', `M ${iconOffset + 4} ${iconOffset + 18} L ${iconOffset + 10} ${iconOffset + 10} L ${iconOffset + 14} ${iconOffset + 14} L ${iconOffset + 20} ${iconOffset + 8} L ${iconOffset + 20} ${iconOffset + 18} Z`)
824
+          .attr('fill', '#60A5FA');
825
+        
826
+        // Sun
827
+        iconGroup.append('circle')
828
+          .attr('cx', iconOffset + 15)
829
+          .attr('cy', iconOffset + 10)
830
+          .attr('r', 2)
831
+          .attr('fill', '#FDE047');
832
+          
833
+      } else if (d.data.path.includes('Downloads')) {
834
+        // Simple download arrow icon
835
+        iconGroup.append('rect')
836
+          .attr('x', iconOffset + 4)
837
+          .attr('y', iconOffset + 14)
838
+          .attr('width', 16)
839
+          .attr('height', 3)
840
+          .attr('fill', '#10B981')
841
+          .attr('stroke', '#059669')
842
+          .attr('stroke-width', 1);
843
+        
844
+        iconGroup.append('rect')
845
+          .attr('x', iconOffset + 11)
846
+          .attr('y', iconOffset + 4)
847
+          .attr('width', 2)
848
+          .attr('height', 10)
849
+          .attr('fill', '#10B981');
850
+        
851
+        iconGroup.append('path')
852
+          .attr('d', `M ${iconOffset + 8} ${iconOffset + 10} L ${iconOffset + 12} ${iconOffset + 14} L ${iconOffset + 16} ${iconOffset + 10}`)
853
+          .attr('fill', 'none')
854
+          .attr('stroke', '#10B981')
855
+          .attr('stroke-width', 2)
856
+          .attr('stroke-linecap', 'round')
857
+          .attr('stroke-linejoin', 'round');
858
+          
859
+      } else if (d.data.path.includes('Desktop')) {
860
+        // Simple monitor icon
861
+        iconGroup.append('rect')
862
+          .attr('x', iconOffset + 3)
863
+          .attr('y', iconOffset + 5)
864
+          .attr('width', 18)
865
+          .attr('height', 12)
866
+          .attr('rx', 1)
867
+          .attr('fill', '#1F2937')
868
+          .attr('stroke', '#111827')
869
+          .attr('stroke-width', 1.5);
870
+        
871
+        iconGroup.append('rect')
872
+          .attr('x', iconOffset + 5)
873
+          .attr('y', iconOffset + 7)
874
+          .attr('width', 14)
875
+          .attr('height', 8)
876
+          .attr('fill', '#60A5FA');
877
+        
878
+        iconGroup.append('rect')
879
+          .attr('x', iconOffset + 10)
880
+          .attr('y', iconOffset + 17)
881
+          .attr('width', 4)
882
+          .attr('height', 3)
883
+          .attr('fill', '#374151');
884
+        
885
+        iconGroup.append('rect')
886
+          .attr('x', iconOffset + 7)
887
+          .attr('y', iconOffset + 20)
888
+          .attr('width', 10)
889
+          .attr('height', 2)
890
+          .attr('rx', 1)
891
+          .attr('fill', '#374151');
892
+      }
893
+      
894
+      // Add subtle animation to icons
895
+      if (iconGroup.node()?.hasChildNodes()) {
896
+        iconGroup
897
+          .style('opacity', 0.8)
898
+          .on('mouseover', function() {
899
+            d3.select(this)
900
+              .transition()
901
+              .duration(200)
902
+              .style('opacity', 1)
903
+              .attr('transform', 'scale(1.2)');
904
+          })
905
+          .on('mouseout', function() {
906
+            d3.select(this)
907
+              .transition()
908
+              .duration(200)
909
+              .style('opacity', 0.8)
910
+              .attr('transform', 'scale(1)');
911
+          });
912
+      }
913
+    });
914
+
915
+    // Add player and mole indicators
435916
     const playerNode = treeNodes.descendants().find(d => d.data.path === playerLocation);
436917
     if (playerNode) {
437918
       const playerGroup = node
438919
         .filter(d => d.data.path === playerLocation)
439920
         .append('g');
440921
 
441
-      // Add player SVG directly on the node
442922
       playerGroup
443923
         .append('image')
444924
         .attr('xlink:href', ICON_CONFIG.paths.player)
@@ -446,41 +926,44 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
446926
         .attr('height', ICON_CONFIG.size)
447927
         .attr('x', ICON_CONFIG.offset)
448928
         .attr('y', ICON_CONFIG.offset)
449
-        .style('pointer-events', 'none');
929
+        .style('pointer-events', 'none')
930
+        .style('filter', 'drop-shadow(0 0 6px rgba(59, 130, 246, 0.8))');
450931
     }
451932
 
452
-    // Add mole indicator with SVG overlaid if game is won
453933
     const moleNode = treeNodes.descendants().find(d => d.data.has_mole);
454934
     if (moleNode) {
455935
       const moleGroup = node
456936
         .filter(d => d.data.has_mole)
457937
         .append('g');
458938
 
459
-      // Add celebration animation ring
460
-      moleGroup
461
-        .append('circle')
462
-        .attr('r', CELEBRATION_CONFIG.ring.startRadius)
463
-        .style('fill', 'none')
464
-        .style('stroke', NODE_CONFIG.colors.mole.fill)
465
-        .style('stroke-width', CELEBRATION_CONFIG.ring.strokeWidth)
466
-        .style('opacity', 0)
467
-        .append('animate')
468
-        .attr('attributeName', 'r')
469
-        .attr('from', CELEBRATION_CONFIG.ring.startRadius)
470
-        .attr('to', CELEBRATION_CONFIG.ring.endRadius)
471
-        .attr('dur', ANIMATION_CONFIG.celebration.duration)
472
-        .attr('repeatCount', ANIMATION_CONFIG.celebration.repeatCount);
939
+      // Celebration rings
940
+      for (let i = 0; i < 3; i++) {
941
+        moleGroup
942
+          .append('circle')
943
+          .attr('r', 15)
944
+          .style('fill', 'none')
945
+          .style('stroke', NODE_CONFIG.colors.mole.pulse)
946
+          .style('stroke-width', 2)
947
+          .style('opacity', 0)
948
+          .transition()
949
+          .delay(i * 300)
950
+          .duration(1500)
951
+          .ease(d3.easeQuadOut)
952
+          .attr('r', 40)
953
+          .style('opacity', 0)
954
+          .on('end', function repeat() {
955
+            d3.select(this)
956
+              .attr('r', 15)
957
+              .style('opacity', 0)
958
+              .transition()
959
+              .duration(1500)
960
+              .ease(d3.easeQuadOut)
961
+              .attr('r', 40)
962
+              .style('opacity', 0)
963
+              .on('end', repeat);
964
+          });
965
+      }
473966
 
474
-      moleGroup
475
-        .select('circle')
476
-        .append('animate')
477
-        .attr('attributeName', 'opacity')
478
-        .attr('from', '1')
479
-        .attr('to', '0')
480
-        .attr('dur', ANIMATION_CONFIG.celebration.duration)
481
-        .attr('repeatCount', ANIMATION_CONFIG.celebration.repeatCount);
482
-
483
-      // Add mole SVG with falling animation when killed
484967
       moleGroup
485968
         .append('image')
486969
         .attr('xlink:href', ICON_CONFIG.paths.mole)
@@ -489,38 +972,35 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
489972
         .attr('x', ICON_CONFIG.offset)
490973
         .attr('y', ICON_CONFIG.offset)
491974
         .style('pointer-events', 'none')
975
+        .style('filter', 'drop-shadow(0 0 6px rgba(239, 68, 68, 0.8))')
492976
         .classed('mole-death', moleKilled);
493977
     }
494978
 
495
-    // Add zoom and pan behavior
979
+    // Zoom behavior
496980
     const zoom = d3.zoom<SVGSVGElement, unknown>()
497981
       .scaleExtent(ZOOM_CONFIG.scaleExtent)
498982
       .on('zoom', (event) => {
499983
         g.attr('transform', event.transform);
500984
       });
501985
 
502
-    // Apply zoom behavior immediately
503986
     svg.call(zoom);
504987
 
505
-    // Helper function to create zoom transform for centering on a node
988
+    // Zoom transform helper
506989
     const getZoomTransform = (node: d3.HierarchyPointNode<TreeNode>, scale: number, offsetX: number = 0, offsetY: number = 0) => {
507990
       const viewBoxCenterX = width / 2;
508991
       const viewBoxCenterY = height / 2;
509992
       
510
-      // Calculate translation to center the node with optional offset
511993
       const translateX = viewBoxCenterX - (node.x + margin.left) * scale + (width * offsetX);
512994
       const translateY = viewBoxCenterY - (node.y + margin.top) * scale + (height * offsetY);
513995
       
514996
       return d3.zoomIdentity.translate(translateX, translateY).scale(scale);
515997
     };
516998
 
517
-    // Animated intro sequence using zoom transitions
999
+    // Handle intro and navigation animations
5181000
     if (playIntro && playerNode) {
519
-      // Start zoomed in on root
5201001
       const rootTransform = getZoomTransform(treeNodes, ZOOM_CONFIG.defaultScale);
5211002
       svg.call(zoom.transform, rootTransform);
5221003
       
523
-      // Calculate full tree view
5241004
       const allNodes = treeNodes.descendants();
5251005
       const xExtent = d3.extent(allNodes, d => d.x) as [number, number];
5261006
       const yExtent = d3.extent(allNodes, d => d.y) as [number, number];
@@ -537,26 +1017,17 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
5371017
       const treeCenter = { x: treeCenterX, y: treeCenterY } as d3.HierarchyPointNode<TreeNode>;
5381018
       const fullTreeTransform = getZoomTransform(treeCenter, fullTreeScale);
5391019
       
540
-      // Find mole location
541
-      const moleNode = treeNodes.descendants().find(d => d.data.has_mole);
5421020
       const moleTransform = moleNode ? getZoomTransform(moleNode, ZOOM_CONFIG.defaultScale) : null;
543
-      
544
-      // Partial tree view (for after showing mole)
5451021
       const partialTreeTransform = getZoomTransform(treeCenter, ZOOM_CONFIG.partialTreeScale);
546
-      
547
-      // Final player position with nudge offset
5481022
       const playerTransform = getZoomTransform(playerNode, ZOOM_CONFIG.defaultScale, 
5491023
                                               ZOOM_CONFIG.nudgeOffset.x, 
5501024
                                               ZOOM_CONFIG.nudgeOffset.y);
5511025
       
552
-      // Animate using zoom transitions with new sequence
5531026
       const phases = ANIMATION_CONFIG.intro.phases;
5541027
       
555
-      // Build the transition chain
5561028
       if (moleTransform) {
5571029
         isAnimatingRef.current = true;
5581030
         
559
-        // Create a single transition and chain all the calls
5601031
         svg.transition()
5611032
           .duration(phases[0].duration)
5621033
           .call(zoom.transform, rootTransform)
@@ -588,7 +1059,6 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
5881059
       } else {
5891060
         isAnimatingRef.current = true;
5901061
         
591
-        // Shorter sequence without mole
5921062
         svg.transition()
5931063
           .duration(phases[0].duration)
5941064
           .call(zoom.transform, rootTransform)
@@ -608,19 +1078,16 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({
6081078
           });
6091079
       }
6101080
     } else if (playerNode && !isAnimatingRef.current) {
611
-      // No intro: position on player with nudge offset
6121081
       const playerTransform = getZoomTransform(playerNode, ZOOM_CONFIG.defaultScale,
6131082
                                               ZOOM_CONFIG.nudgeOffset.x,
6141083
                                               ZOOM_CONFIG.nudgeOffset.y);
6151084
       
6161085
       if (isNavigation) {
617
-        // Smooth transition for navigation moves
6181086
         svg.transition()
6191087
           .duration(ANIMATION_CONFIG.navigation.duration)
6201088
           .ease(ANIMATION_CONFIG.navigation.easing)
6211089
           .call(zoom.transform, playerTransform);
6221090
       } else {
623
-        // Instant positioning for initial load
6241091
         svg.call(zoom.transform, playerTransform);
6251092
       }
6261093
     }