1/** 2 * tutorial-ui.js 3 * Manages tutorial UI components and rendering 4 */ 5class TutorialUIManager { 6 constructor(tutorialService) { 7 this.tutorialService = tutorialService; 8 this.game = tutorialService.game; 9 this.board = tutorialService.board; 10 11 // Tutorial UI elements 12 this.tutorialOverlay = null; 13 this.messageBox = null; 14 this.progressIndicator = null; 15 this.continueButton = null; 16 this.skipButton = null; 17 this.tryAgainButton = null; 18 this.tutorialControls = null; 19 20 // Original game controls container to restore later 21 this.originalGameControls = null; 22 23 // Event handlers for proper cleanup 24 this.continueHandler = null; 25 this.skipHandler = null; 26 this.tryAgainHandler = null; 27 } 28 29 /** 30 * Create and setup tutorial UI elements 31 */ 32 setupTutorialUI() { 33 this.createTutorialElements(); 34 this.ensureTutorialStylesLoaded(); 35 this.setupTutorialControls(); 36 } 37 38 /** 39 * Create tutorial UI elements 40 */ 41 createTutorialElements() { 42 // Clean up any existing elements first 43 this.cleanupTutorialUI(); 44 45 // Create overlay container 46 this.tutorialOverlay = document.createElement('div'); 47 this.tutorialOverlay.id = 'tutorial-overlay'; 48 this.tutorialOverlay.className = 'tutorial-overlay'; 49 50 // Create message box 51 this.messageBox = document.createElement('div'); 52 this.messageBox.id = 'tutorial-message'; 53 this.messageBox.className = 'tutorial-message'; 54 55 // Add message box to overlay 56 this.tutorialOverlay.appendChild(this.messageBox); 57 58 // Add overlay to game screen 59 const gameScreen = document.getElementById('game-screen'); 60 if (gameScreen) { 61 gameScreen.appendChild(this.tutorialOverlay); 62 } 63 } 64 65 /** 66 * Setup tutorial controls by replacing the game controls 67 */ 68 setupTutorialControls() { 69 // Find the game controls container 70 const gameControls = document.querySelector('.game-controls'); 71 if (!gameControls) { 72 console.error('Game controls container not found'); 73 return; 74 } 75 76 // Save original game controls to restore later 77 this.originalGameControls = gameControls.cloneNode(true); 78 79 // Create tutorial controls 80 this.tutorialControls = document.createElement('div'); 81 this.tutorialControls.className = 'tutorial-controls'; 82 83 // Create continue button 84 this.continueButton = document.createElement('button'); 85 this.continueButton.id = 'tutorial-continue-btn'; 86 this.continueButton.className = 'tutorial-btn'; 87 this.continueButton.textContent = 'Continue'; 88 this.continueButton.style.display = 'none'; 89 90 // Create try again button 91 this.tryAgainButton = document.createElement('button'); 92 this.tryAgainButton.id = 'tutorial-try-again-btn'; 93 this.tryAgainButton.className = 'tutorial-btn'; 94 this.tryAgainButton.textContent = 'Try Again'; 95 this.tryAgainButton.style.display = 'none'; 96 97 // Create skip button 98 this.skipButton = document.createElement('button'); 99 this.skipButton.id = 'tutorial-skip-btn'; 100 this.skipButton.className = 'tutorial-btn'; 101 this.skipButton.textContent = 'Skip Tutorial'; 102 103 // Add event handlers 104 this.continueHandler = () => { 105 if (this.tutorialService && this.tutorialService.isActive) { 106 this.tutorialService.advanceToNextStep(); 107 } 108 }; 109 110 this.skipHandler = () => { 111 if (this.tutorialService && this.tutorialService.isActive) { 112 this.tutorialService.end(); 113 } 114 }; 115 116 this.tryAgainHandler = () => { 117 if (this.tutorialService && this.tutorialService.isActive) { 118 this.hideTryAgainButton(); 119 this.tutorialService.stepManager.retryCurrentStep(); 120 } 121 }; 122 123 this.continueButton.addEventListener('click', this.continueHandler); 124 this.skipButton.addEventListener('click', this.skipHandler); 125 this.tryAgainButton.addEventListener('click', this.tryAgainHandler); 126 127 // Add buttons to controls 128 this.tutorialControls.appendChild(this.continueButton); 129 this.tutorialControls.appendChild(this.tryAgainButton); 130 this.tutorialControls.appendChild(this.skipButton); 131 132 // Replace game controls with tutorial controls 133 gameControls.innerHTML = ''; 134 gameControls.appendChild(this.tutorialControls); 135 136 // Show the game controls container to ensure tutorial controls are visible 137 gameControls.style.display = 'flex'; 138 } 139 140 /** 141 * Ensure tutorial styles are loaded 142 */ 143 ensureTutorialStylesLoaded() { 144 // Check if the styles are already loaded 145 if (!document.getElementById('tutorial-external-css')) { 146 // Create link element to load external CSS 147 const link = document.createElement('link'); 148 link.id = 'tutorial-external-css'; 149 link.rel = 'stylesheet'; 150 link.type = 'text/css'; 151 link.href = 'css/tutorial-styles.css'; 152 153 // Add to head 154 document.head.appendChild(link); 155 } 156 } 157 158 /** 159 * Hide game controls during tutorial 160 */ 161 hideGameControls() { 162 // This function is no longer needed as we're replacing the controls 163 // rather than hiding them 164 } 165 166 /**
167 * Restore game controls 168 */ 169 restoreGameControls() { 170 // Restore original game controls if they exist 171 if (this.originalGameControls) { 172 const gameControls = document.querySelector('.game-controls'); 173 if (gameControls) { 174 // Replace with the original controls 175 gameControls.innerHTML = ''; 176 177 // Clone all child nodes from original game controls 178 Array.from(this.originalGameControls.childNodes).forEach(node => { 179 gameControls.appendChild(node.cloneNode(true)); 180 }); 181 182 // Restore event listeners for undo/redo buttons 183 const undoBtn = gameControls.querySelector('#undo-btn'); 184 const redoBtn = gameControls.querySelector('#redo-btn'); 185 const menuBtn = gameControls.querySelector('#menu-btn'); 186 187 if (undoBtn && this.game.undoMove) { 188 undoBtn.addEventListener('click', this.game.undoMove.bind(this.game)); 189 } 190 191 if (redoBtn && this.game.redoMove) { 192 redoBtn.addEventListener('click', this.game.redoMove.bind(this.game)); 193 } 194 195 if (menuBtn && this.game.ui && this.game.ui.openMenu) { 196 menuBtn.addEventListener('click', this.game.ui.openMenu.bind(this.game.ui)); 197 } 198 } 199 200 // Clear the reference 201 this.originalGameControls = null; 202 } 203 } 204 205 /** 206 * Show the game screen 207 */ 208 showGameScreen() { 209 // Hide all screens first to avoid potential conflicts 210 const screens = document.querySelectorAll('.screen'); 211 screens.forEach(screen => { 212 if (screen) screen.classList.add('hidden'); 213 }); 214 215 // Show game screen specifically 216 const gameScreen = document.getElementById('game-screen'); 217 if (gameScreen) { 218 gameScreen.classList.remove('hidden'); 219 } 220 221 // Hide any opponent display from tournament mode 222 const opponentDisplay = document.getElementById('opponent-display'); 223 if (opponentDisplay) { 224 opponentDisplay.classList.add('hidden'); 225 } 226 } 227 228 /** 229 * Show the main menu 230 */ 231 showMainMenu() { 232 // Hide all screens first 233 const screens = document.querySelectorAll('.screen'); 234 screens.forEach(screen => { 235 if (screen) screen.classList.add('hidden'); 236 }); 237 238 // Show main menu 239 const mainMenu = document.getElementById('main-menu'); 240 if (mainMenu) { 241 mainMenu.classList.remove('hidden'); 242 243 // Focus on the first button in the main menu for better UX 244 setTimeout(() => { 245 const firstButton = mainMenu.querySelector('button'); 246 if (firstButton) { 247 firstButton.focus(); 248 } 249 }, 100); 250 } 251 } 252 253 /** 254 * Show instructions in the message box with integrated step progress 255 */ 256 showInstructions(title, message, currentStep, totalSteps) { 257 if (!this.messageBox) return; 258 259 // Format the title with step count 260 const titleElement = `<span class="tutorial-step-title">Tutorial ${currentStep} of ${totalSteps}: ${title}</span>`; 261 262 // Convert newlines to HTML breaks 263 const formattedMessage = message.replace(/\n/g, '<br>'); 264 265 const content = `${titleElement}${formattedMessage}`; 266 this.messageBox.innerHTML = content; 267 } 268 269 /** 270 * Show the continue button 271 */ 272 showContinueButton() { 273 if (this.continueButton) { 274 this.continueButton.style.display = 'block'; 275 if (this.tryAgainButton) { 276 this.tryAgainButton.style.display = 'none'; // Hide try again button when showing continue 277 } 278 279 // Focus the continue button for better accessibility 280 setTimeout(() => { 281 this.continueButton.focus(); 282 }, 100); 283 } 284 } 285 286 /** 287 * Hide the continue button 288 */ 289 hideContinueButton() { 290 if (this.continueButton) { 291 this.continueButton.style.display = 'none'; 292 } 293 } 294 295 /** 296 * Show the try again button 297 */ 298 showTryAgainButton() { 299 if (this.tryAgainButton) { 300 this.tryAgainButton.style.display = 'block'; 301 if (this.continueButton) { 302 this.continueButton.style.display = 'none'; // Hide continue button when showing try again 303 } 304 305 // Focus the try again button for better accessibility 306 setTimeout(() => { 307 this.tryAgainButton.focus(); 308 }, 100); 309 } 310 } 311 312 /** 313 * Hide the try again button 314 */ 315 hideTryAgainButton() { 316 if (this.tryAgainButton) { 317 this.tryAgainButton.style.display = 'none'; 318 } 319 } 320 321 /** 322 * Show completion message 323 */ 324 showCompletionMessage() { 325 // Update message box 326 this.showInstructions("Tutorial Complete", ` 327 <div class="tutorial-complete"> 328 <div class="tutorial-complete-icon">ð</div> 329 <p>You win the match when opponents has no tokens to move.</p> 330 <p>Try now the Tournament Mode!</p> 331 </div> 332 `, this.tutorialService.stepManager.getTotalSteps(), this.tutorialService.stepManager.getTotalSteps()); 333 334 // Hide the continue and skip buttons since we're returning to menu automatically 335 if (this.continueButton) this.continueButton.style.display = 'none'; 336 if (this.skipButton) this.skipButton.style.display = 'none'; 337 } 338 339 /** 340 * Clean up all tutorial UI elements 341 */ 342 cleanupTutorialUI() { 343 // Remove event listeners 344 if (this.continueButton) { 345 this.continueButton.removeEventListener('click', this.continueHandler); 346 } 347 348 if (this.skipButton) { 349 this.skipButton.removeEventListener('click', this.skipHandler); 350 } 351 352 if (this.tryAgainButton) { 353 this.tryAgainButton.removeEventListener('click', this.tryAgainHandler); 354 } 355 356 // Remove overlay if it exists 357 if (this.tutorialOverlay && this.tutorialOverlay.parentNode) { 358 this.tutorialOverlay.parentNode.removeChild(this.tutorialOverlay); 359 } 360 361 // Restore original game controls 362 this.restoreGameControls(); 363 364 // Reset references 365 this.tutorialOverlay = null; 366 this.messageBox = null; 367 this.continueButton = null; 368 this.skipButton = null; 369 this.tryAgainButton = null; 370 this.tutorialControls = null; 371 this.continueHandler = null; 372 this.skipHandler = null; 373 this.tryAgainHandler = null; 374 375 // Remove any tutorial classes from cells 376 this.clearHighlights(); 377 378 // Remove visual guidance 379 this.clearVisualGuidance(); 380 } 381 382 /** 383 * Clear all highlights 384 */ 385 clearHighlights() { 386 387 // Clear cell highlights 388 document.querySelectorAll('.cell.tutorial-highlight').forEach(cell => { 389 cell.classList.remove('tutorial-highlight'); 390 }); 391 392 // Clear token highlights 393 document.querySelectorAll('.token.tutorial-active').forEach(token => { 394 token.classList.remove('tutorial-active'); 395 }); 396 397 // Clear inactive token highlights 398 document.querySelectorAll('.token.tutorial-inactive-highlight').forEach(token => { 399 token.classList.remove('tutorial-inactive-highlight'); 400 }); 401 402 // Only remove last-move-indicator elements if in active tutorial 403 document.querySelectorAll('.last-move-indicator').forEach(el => { 404 if (el.parentNode) { 405 el.parentNode.removeChild(el); 406 } 407 }); 408 } 409 410 /** 411 * Clear visual guidance elements 412 */ 413 clearVisualGuidance() { 414 // Remove paths 415 const paths = document.querySelectorAll('.tutorial-path'); 416 paths.forEach(path => { 417 if (path.parentNode) { 418 path.parentNode.removeChild(path); 419 } 420 }); 421 } 422 423 /** 424 * Highlight specific positions 425 */ 426 highlightPositions(positions) { 427 if (!positions || positions.length === 0) return; 428 429 positions.forEach(pos => { 430 // Check position validity 431 if (!pos || typeof pos.row !== 'number' || typeof pos.col !== 'number') return; 432 433 // Find the cell 434 const cell = this.board.boardElement.querySelector( 435 `.cell[data-row="${pos.row}"][data-col="${pos.col}"]` 436 ); 437 438 if (cell) { 439 // Add highlight class 440 cell.classList.add('tutorial-highlight'); 441 442 // If there's a token, highlight it too 443 const token = cell.querySelector('.token'); 444 if (token) { 445 token.classList.add('tutorial-active'); 446 } 447 } 448 }); 449 } 450 451 /** 452 * Highlight inactive tokens 453 */ 454 highlightInactiveToken() { 455 // Find all inactive tokens 456 for (let row = 0; row < this.board.size; row++) { 457 for (let col = 0; col < this.board.size; col++) { 458 const token = this.board.getTokenAt(row, col); 459 if (token && !token.isActive && !token.isCaptured) { 460 // Find the cell 461 const cell = this.board.boardElement.querySelector( 462 `.cell[data-row="${row}"][data-col="${col}"]` 463 ); 464 465 if (cell) { 466 // Find the token element 467 const tokenElement = cell.querySelector('.token'); 468 if (tokenElement) { 469 tokenElement.classList.add('tutorial-inactive-highlight'); 470 } 471 } 472 } 473 } 474 } 475 } 476 477 /** 478 * Highlight captured tokens 479 */ 480 highlightCapturedToken() { 481 // Find all captured tokens 482 for (let row = 0; row < this.board.size; row++) { 483 for (let col = 0; col < this.board.size; col++) { 484 const token = this.board.getTokenAt(row, col); 485 if (token && token.isCaptured) { 486 // Find the cell 487 const cell = this.board.boardElement.querySelector( 488 `.cell[data-row="${row}"][data-col="${col}"]` 489 ); 490 491 if (cell) { 492 // Highlight the cell 493 cell.classList.add('tutorial-highlight'); 494 495 // Find the token element 496 const tokenElement = cell.querySelector('.token'); 497 if (tokenElement) { 498 tokenElement.classList.add('tutorial-active'); 499 } 500 } 501 } 502 } 503 } 504 } 505}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.