TutorialCommands.ts
1 import { Plugin } from 'obsidian'; 2 import { UIService } from '../../core/services/ui-service'; 3 import { serviceManager } from '../../core/services/service-manager'; 4 import { tutorialService } from './TutorialService'; 5 import { TutorialModal } from './TutorialModal'; 6 import { useInterBrainStore } from '../../core/store/interbrain-store'; 7 import { calculateProjectedEdgePositions, calculateTextPositionNextToNode } from './utils/projection'; 8 import { musicService } from './services/music-service'; 9 10 /** 11 * Register tutorial commands for onboarding system 12 */ 13 export function registerTutorialCommands(plugin: Plugin, uiService: UIService): void { 14 15 // Start MVP Tutorial (new system) 16 plugin.addCommand({ 17 id: 'start-mvp-tutorial', 18 name: 'Start MVP Tutorial', 19 callback: () => { 20 console.log('đ Starting MVP tutorial'); 21 const store = useInterBrainStore.getState(); 22 store.startTutorial(); 23 uiService.showInfo('Tutorial started - watch DreamSpace'); 24 } 25 }); 26 27 // Stop MVP Tutorial 28 plugin.addCommand({ 29 id: 'stop-mvp-tutorial', 30 name: 'Stop MVP Tutorial', 31 callback: () => { 32 console.log('âšī¸ Stopping MVP tutorial'); 33 const store = useInterBrainStore.getState(); 34 store.endTutorial(); 35 uiService.showInfo('Tutorial stopped'); 36 } 37 }); 38 39 // Start Tutorial in 3D space (legacy) 40 plugin.addCommand({ 41 id: 'start-tutorial', 42 name: 'Start Tutorial (Legacy)', 43 callback: () => { 44 console.log('đ Starting tutorial in 3D space'); 45 tutorialService.start(); 46 uiService.showInfo('Tutorial started - watch DreamSpace'); 47 } 48 }); 49 50 // Start Tutorial in modal 51 plugin.addCommand({ 52 id: 'start-tutorial-modal', 53 name: 'Start Tutorial (Modal)', 54 callback: () => { 55 console.log('đ Starting tutorial in modal'); 56 tutorialService.start(); 57 const modal = new TutorialModal(plugin.app); 58 modal.open(); 59 } 60 }); 61 62 // Reset Tutorial (Debug) 63 plugin.addCommand({ 64 id: 'reset-tutorial', 65 name: 'Reset Tutorial (Debug)', 66 callback: () => { 67 console.log('đ Resetting tutorial state'); 68 tutorialService.reset(); 69 uiService.showInfo('Tutorial state reset. Run Start Tutorial to begin again.'); 70 } 71 }); 72 73 // Skip Tutorial 74 plugin.addCommand({ 75 id: 'skip-tutorial', 76 name: 'Skip Tutorial', 77 callback: () => { 78 console.log('âī¸ Skipping tutorial'); 79 tutorialService.skip(); 80 uiService.showInfo('Tutorial skipped'); 81 } 82 }); 83 84 // Test Golden Dot - Arc (Debug) 85 plugin.addCommand({ 86 id: 'test-golden-dot', 87 name: 'Test Golden Dot - Arc (Debug)', 88 callback: () => { 89 console.log('⨠Testing golden dot animation (arc)'); 90 91 // Animate from left to right with default arc 92 tutorialService.animateGoldenDot({ 93 from: [-15, 0, -30], 94 to: [15, 0, -30], 95 duration: 3, 96 size: 100, 97 easing: 'easeInOut' 98 }); 99 100 uiService.showInfo('Golden dot test (arc) - watch DreamSpace'); 101 } 102 }); 103 104 // Test Golden Dot - Linear (Debug) 105 plugin.addCommand({ 106 id: 'test-golden-dot-linear', 107 name: 'Test Golden Dot - Linear (Debug)', 108 callback: () => { 109 console.log('⨠Testing golden dot animation (linear)'); 110 111 const from: [number, number, number] = [-15, 0, -30]; 112 const to: [number, number, number] = [15, 0, -30]; 113 114 // Linear path: control points on the line between from and to 115 const cp1: [number, number, number] = [ 116 from[0] + (to[0] - from[0]) * 0.33, 117 from[1] + (to[1] - from[1]) * 0.33, 118 from[2] + (to[2] - from[2]) * 0.33, 119 ]; 120 const cp2: [number, number, number] = [ 121 from[0] + (to[0] - from[0]) * 0.66, 122 from[1] + (to[1] - from[1]) * 0.66, 123 from[2] + (to[2] - from[2]) * 0.66, 124 ]; 125 126 tutorialService.animateGoldenDot({ 127 from, 128 to, 129 controlPoints: [cp1, cp2], 130 duration: 3, 131 size: 100, 132 easing: 'easeInOut' 133 }); 134 135 uiService.showInfo('Golden dot test (linear) - watch DreamSpace'); 136 } 137 }); 138 139 // Test Golden Dot - Node to Node (Debug) 140 plugin.addCommand({ 141 id: 'test-golden-dot-nodes', 142 name: 'Test Golden Dot - Random Dreamers (Debug)', 143 callback: () => { 144 console.log('⨠Testing golden dot animation between random Dreamer nodes'); 145 146 const store = useInterBrainStore.getState(); 147 148 // Filter to only Dreamer nodes (type === 'dreamer') 149 const dreamerIds = Array.from(store.dreamNodes.entries()) 150 .filter(([_, data]) => data.node.type === 'dreamer') 151 .map(([id]) => id); 152 153 if (dreamerIds.length < 2) { 154 uiService.showError('Need at least 2 Dreamer nodes for this test'); 155 return; 156 } 157 158 // Pick two random different Dreamer nodes 159 const shuffled = dreamerIds.sort(() => Math.random() - 0.5); 160 const fromNodeId = shuffled[0]; 161 const toNodeId = shuffled[1]; 162 163 const fromNode = store.dreamNodes.get(fromNodeId); 164 const toNode = store.dreamNodes.get(toNodeId); 165 166 // Get actual rendered positions from orchestrator (not store positions) 167 const orchestrator = serviceManager.getSpatialOrchestrator(); 168 if (!orchestrator) { 169 console.error('⨠SpatialOrchestrator not available'); 170 uiService.showError('Orchestrator not ready - try again after view is loaded'); 171 return; 172 } 173 174 const fromPos = orchestrator.getNodeCurrentPosition(fromNodeId); 175 const toPos = orchestrator.getNodeCurrentPosition(toNodeId); 176 177 if (!fromPos || !toPos) { 178 console.error('⨠Missing rendered positions:', { fromPos, toPos }); 179 uiService.showError('Node positions not available - nodes may not be rendered'); 180 return; 181 } 182 183 // Calculate edge positions (at hit sphere boundaries) and project to Z=-30 184 // This ensures: 185 // 1. Dot starts/ends at node edges, not centers 186 // 2. The slow easing animation happens outside the node's visual footprint 187 // 3. Hit detection still works (positions are slightly inside boundaries) 188 const DOT_Z_PLANE = -30; 189 const { from, to } = calculateProjectedEdgePositions(fromPos, toPos, DOT_Z_PLANE); 190 191 console.log(`⨠Animating from "${fromNode?.node.name}" to "${toNode?.node.name}"`); 192 console.log(`⨠From node position:`, fromPos); 193 console.log(`⨠To node position:`, toPos); 194 console.log(`⨠Edge-projected from:`, from); 195 console.log(`⨠Edge-projected to:`, to); 196 197 // Timing constants 198 const START_DELAY = 1000; // Show start node glow for 1s before dot moves 199 const DOT_DURATION = 3; // Dot travel time in seconds 200 const END_DELAY = 1000; // Show end node glow for 1s after dot arrives 201 202 // Pre-highlight start node before animation begins 203 store.setHighlightedNodeId(fromNodeId); 204 205 // After start delay, begin the dot animation 206 setTimeout(() => { 207 tutorialService.animateGoldenDot({ 208 from, 209 to, 210 duration: DOT_DURATION, 211 size: 120, 212 easing: 'easeInOut', 213 // Hit detection will automatically trigger hover on these nodes 214 hitDetectionNodeIds: [fromNodeId, toNodeId] 215 }); 216 217 // After dot arrives, keep end glow for END_DELAY then clear 218 setTimeout(() => { 219 setTimeout(() => { 220 store.setHighlightedNodeId(null); 221 }, END_DELAY); 222 }, DOT_DURATION * 1000); 223 }, START_DELAY); 224 225 uiService.showInfo(`Golden dot: ${fromNode?.node.name} â ${toNode?.node.name}`); 226 } 227 }); 228 229 // Test Text Next to Node (Debug) 230 plugin.addCommand({ 231 id: 'test-text-next-to-node', 232 name: 'Test Text Next to Node (Debug)', 233 callback: () => { 234 console.log('đ Testing text animation next to random Dreamer node'); 235 236 const store = useInterBrainStore.getState(); 237 238 // Filter to only Dreamer nodes 239 const dreamerIds = Array.from(store.dreamNodes.entries()) 240 .filter(([_, data]) => data.node.type === 'dreamer') 241 .map(([id]) => id); 242 243 if (dreamerIds.length === 0) { 244 uiService.showError('No Dreamer nodes found'); 245 return; 246 } 247 248 // Pick a random Dreamer node 249 const randomIndex = Math.floor(Math.random() * dreamerIds.length); 250 const nodeId = dreamerIds[randomIndex]; 251 const nodeData = store.dreamNodes.get(nodeId); 252 253 // Get actual rendered position from orchestrator 254 const orchestrator = serviceManager.getSpatialOrchestrator(); 255 if (!orchestrator) { 256 console.error('đ SpatialOrchestrator not available'); 257 uiService.showError('Orchestrator not ready - try again after view is loaded'); 258 return; 259 } 260 261 const nodePos = orchestrator.getNodeCurrentPosition(nodeId); 262 if (!nodePos) { 263 console.error('đ Node position not available'); 264 uiService.showError('Node position not available - node may not be rendered'); 265 return; 266 } 267 268 // Calculate text position next to node (perspective-correct) 269 const TEXT_Z_PLANE = -30; 270 const textPosition = calculateTextPositionNextToNode(nodePos, TEXT_Z_PLANE, 'right', 12); 271 272 console.log(`đ Showing "Click me!" next to "${nodeData?.node.name}"`); 273 console.log(`đ Node position:`, nodePos); 274 console.log(`đ Text position:`, textPosition); 275 276 // Also highlight the node for visual reference 277 store.setHighlightedNodeId(nodeId); 278 279 // Show the text animation 280 tutorialService.showText({ 281 text: 'Click me!', 282 position: textPosition, 283 fontSize: 36, 284 duration: 5000, // Show for 5 seconds 285 }); 286 287 // Clear highlight after text disappears 288 setTimeout(() => { 289 store.setHighlightedNodeId(null); 290 }, 5000); 291 292 uiService.showInfo(`Text shown next to ${nodeData?.node.name}`); 293 } 294 }); 295 296 // Test Music Playback (Debug) 297 plugin.addCommand({ 298 id: 'test-music', 299 name: 'Test Music Playback (Debug)', 300 callback: () => { 301 console.log('đĩ Testing tutorial music playback'); 302 303 const app = serviceManager.getApp(); 304 if (!app) { 305 uiService.showError('App not available'); 306 return; 307 } 308 309 // Initialize and play using Obsidian's resource path API 310 musicService.initialize(app); 311 musicService.play(2000); // 2 second fade-in 312 313 uiService.showInfo('Music started - run "Stop Music" to stop'); 314 } 315 }); 316 317 // Stop Music Playback (Debug) 318 plugin.addCommand({ 319 id: 'stop-music', 320 name: 'Stop Music Playback (Debug)', 321 callback: () => { 322 console.log('đĩ Stopping tutorial music'); 323 musicService.stop(1500); // 1.5 second fade-out 324 uiService.showInfo('Music stopping with fade-out'); 325 } 326 }); 327 328 // Show Tutorial Portal (Entry Screen) 329 plugin.addCommand({ 330 id: 'show-tutorial-portal', 331 name: 'Show Tutorial Portal', 332 callback: () => { 333 console.log('đ Showing tutorial portal'); 334 const store = useInterBrainStore.getState(); 335 store.showTutorialPortal(); 336 } 337 }); 338 }