/ src / features / tutorial / TutorialCommands.ts
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  }