Created
January 29, 2026 07:55
-
-
Save VarunBatraIT/e82dfc7d3e966ca4ca76fa17362e0688 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Lichess Game Play with Questions and Timer Quotes | |
| // @namespace vm-lichess-game-play | |
| // @version 1.1 | |
| // @description Interactive questions + timer quotes for Lichess | |
| // @match https://lichess.org/* | |
| // @run-at document-start | |
| // @grant none | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| // ======== OPENING PRINCIPLES ======== | |
| const openingPrinciples = [ | |
| "🎯 Control the Center", | |
| "🐴 Develop Minor Pieces Early", | |
| "👸 Don't Bring the Queen Out Too Early", | |
| "🚫 Avoid Unnecessary Pawn Moves", | |
| "🏰 Castle Early", | |
| "👑 King Safety First", | |
| "⭕ Once Developed, break the center", | |
| "🔄 Avoid Moving Same Piece Twice in Opening", | |
| "🛡️ Keep Your Pieces Protected", | |
| "♟️ Watch for f pawn", | |
| "🏁 Finish Development Before Attacking", | |
| "🔚 Pawn structures will define the end-game" | |
| ]; | |
| // ======== MIDGAME PRINCIPLES ======== | |
| const midgamePrinciples = [ | |
| "🔗 Connect the Rooks", | |
| "⚠️ To Take is a Mistake if Position Worsens", | |
| "↗️ Put Bishops on Long Diagonals", | |
| "📂 Put Rooks on Open/Half-Open Files", | |
| "🎪 Put Knights in the Center", | |
| "♞ Put knights on outposts", | |
| "🤝 Coordinate Your Pieces", | |
| "🌟 Rooks on the Seventh Rank", | |
| "🐴 Keep Your Pieces Active", | |
| "🧱 Maintain Pawn Structure", | |
| "🏰 Don't Move Pawns in Front of Castled King", | |
| "⬅️ Capture Pawns Towards Center", | |
| "👁️ Watch Your Opponent's Threats", | |
| "🎯 Control Key Squares", | |
| "⚖️ Trade Pieces When Ahead in Material", | |
| "💎 Avoid Trading Your Strong Pieces", | |
| "🚀 Create Passed Pawns", | |
| "🛑 Avoid Creating Weaknesses", | |
| "🔍 Look for Tactical Opportunities", | |
| "⚖️ When Behind in Material, Avoid Unnecessary Trades", | |
| "🔁 Improve Your Worst-Placed Piece", | |
| "📏 Use Space Advantage Wisely", | |
| "🧮 Evaluate Exchanges: Material vs Activity vs Structure", | |
| "🧱 Fix Opponent Weaknesses Before Creating Your Own", | |
| "🪤 Prophylaxis: Ask 'What Does My Opponent Want?'", | |
| "⚔️ Avoid Attacking Without Development Advantage" | |
| ]; | |
| // ======== ENDGAME PRINCIPLES ======== | |
| const endgamePrinciples = [ | |
| "👑 In end-game King is a piece", | |
| "📉 Simplify Into Winning Endgames, Not Just Any Endgame", | |
| "🚶 Activate the King Early in the Endgame", | |
| "♟️ Passed Pawns Must Be Pushed", | |
| "📂 Rook Activity Over Pawn Grabbing", | |
| "🎯 Centralize Pieces in the Endgame" | |
| ]; | |
| // ======== GENERAL PRINCIPLES ======== | |
| const generalPrinciples = [ | |
| "🧠 Think Ahead and Plan Your Moves", | |
| "⏰ Use All Your Time Wisely", | |
| "😌 Stay Calm Under Time Pressure", | |
| "🔍 Calculate Forcing Moves First (Checks, Captures, Threats)", | |
| "🧠 Improve Position Before Looking for Tactics" | |
| ]; | |
| // ======== MERGED PRINCIPLES ======== | |
| const principles = [ | |
| ...openingPrinciples, | |
| ...midgamePrinciples, | |
| ...endgamePrinciples, | |
| ...generalPrinciples | |
| ]; | |
| // ======== MOTIVATIONAL MESSAGES ======== | |
| const motivationalMessages = [ | |
| 'Good Luck! 🍀', | |
| 'World is Yours! 🌍', | |
| 'You Got This! 💪', | |
| 'Checkmate Time! ♟️', | |
| 'Think Like a Champion! 🏆', | |
| 'Play Your Best! ⭐', | |
| 'Trust Your Skills! 🎯', | |
| 'Time to Shine! ✨', | |
| 'Victory Awaits! 👑', | |
| 'Make Your Move! 🚀', | |
| 'Stay Focused! 🔥', | |
| 'Believe in Yourself! 💫', | |
| 'Game On! 🎮', | |
| 'You\'re Ready! 💯' | |
| ]; | |
| // ======== CONFIG ======== | |
| const questions = [ | |
| { text: "⚠️ Spot <span style='color:#fef08a; font-weight:bold; font-size:20px; text-shadow: 2px 2px 3px rgba(0,0,0,0.8);'>Immediate Threats</span>" }, | |
| { text: "🎯 Count <span style='color:#fef08a; font-weight:bold; font-size:20px; text-shadow: 2px 2px 3px rgba(0,0,0,0.8);'>Undefended Materials</span>" }, | |
| { text: "👁️ See all <span style='color:#fef08a; font-weight:bold; font-size:20px; text-shadow: 2px 2px 3px rgba(0,0,0,0.8);'>Check Angles</span>" }, | |
| { text: "💀 Who are <span style='color:#fef08a; font-weight:bold; font-size:20px; text-shadow: 2px 2px 3px rgba(0,0,0,0.8);'>Hanging</span>?" }, | |
| { text: "📌 Find <span style='color:#fef08a; font-weight:bold; font-size:20px; text-shadow: 2px 2px 3px rgba(0,0,0,0.8);'>Pins</span>" }, | |
| { text: "🍴 Look for <span style='color:#fef08a; font-weight:bold; font-size:20px; text-shadow: 2px 2px 3px rgba(0,0,0,0.8);'>Forks</span>" }, | |
| { text: "🗡️ Are there <span style='color:#fef08a; font-weight:bold; font-size:20px; text-shadow: 2px 2px 3px rgba(0,0,0,0.8);'>Skewers</span>" }, | |
| { text: "💥 Watch-out for <span style='color:#fef08a; font-weight:bold; font-size:20px; text-shadow: 2px 2px 3px rgba(0,0,0,0.8);'>Discovered Attacks</span>" }, | |
| { text: "🔍 Careful for <span style='color:#fef08a; font-weight:bold; font-size:20px; text-shadow: 2px 2px 3px rgba(0,0,0,0.8);'>Nearby Pieces</span>" }, | |
| { text: "🤝 Ensure <span style='color:#fef08a; font-weight:bold; font-size:20px; text-shadow: 2px 2px 3px rgba(0,0,0,0.8);'>Pieces Coordination</span>" }, | |
| { text: "👑 Remember <span style='color:#fef08a; font-weight:bold; font-size:20px; text-shadow: 2px 2px 3px rgba(0,0,0,0.8);'>Kings Safety</span>" }, | |
| { text: "♟️ Check <span style='color:#fef08a; font-weight:bold; font-size:20px; text-shadow: 2px 2px 3px rgba(0,0,0,0.8);'>Pawn Chains</span>!!" }, | |
| { text: "💡 Aha <span style='color:#fef08a; font-weight:bold; font-size:20px; text-shadow: 2px 2px 3px rgba(0,0,0,0.8);'>Pieces Duties</span>!!" }, | |
| { text: "👼 Always <span style='color:#fef08a; font-weight:bold; font-size:20px; text-shadow: 2px 2px 3px rgba(0,0,0,0.8);'>Improve Position</span>!!" }, | |
| { text: "🏠 Most importantly <span style='color:#fef08a; font-weight:bold; font-size:20px; text-shadow: 2px 2px 3px rgba(0,0,0,0.8);'>Safe Place</span>" } | |
| ]; | |
| const timerQuotes = [ | |
| "Best Moves take Time", | |
| "Move soon, Lose soon", | |
| "Patience pays off", | |
| "Think first, act smart", | |
| "Stay calm, play on", | |
| "Chess is a marathon, not a sprint", | |
| "Plan, then execute", | |
| "Focus, then move" | |
| ]; | |
| // ======== GLOBAL STATE ======== | |
| let currentQuestion = 0; | |
| let currentPrinciple = 0; | |
| let timerInterval = null; | |
| let timerElapsed = 0; | |
| // Principles auto-advance state | |
| let principleInterval = null; | |
| const PRINCIPLE_AUTO_SECONDS = 10; | |
| let principleSecondsLeft = PRINCIPLE_AUTO_SECONDS; | |
| let lastActiveMove = ''; | |
| // ======== FONT SIZE CONSTANTS ======== | |
| const FONT_SIZE_QUESTION = '22px'; | |
| const FONT_SIZE_PRINCIPLE = '18px'; | |
| const FONT_SIZE_PRINCIPLE_TIMER = '16px'; | |
| const FONT_SIZE_BUTTON_LARGE = '14px'; | |
| const FONT_SIZE_BUTTON_SMALL = '10px'; | |
| const FONT_SIZE_TIMER = '20px'; | |
| const FONT_SIZE_QUOTE = '15px'; | |
| // ======== DRAWING BRUSH COLORS ======== | |
| // These match Lichess/Chessground brush colors | |
| const DRAWING_BRUSHES = [ | |
| { name: 'Green', color: '#15781B', key: 'green' }, | |
| { name: 'Red', color: '#882020', key: 'red' }, | |
| { name: 'Blue', color: '#003088', key: 'blue' }, | |
| { name: 'Yellow', color: '#e68f00', key: 'yellow' } | |
| ]; | |
| // Currently selected brush for drawing | |
| let selectedBrush = DRAWING_BRUSHES[0]; | |
| let drawingMode = false; | |
| let drawStartPos = null; | |
| // ======== UTILITY FUNCTIONS ======== | |
| function randomQuote() { | |
| return timerQuotes[Math.floor(Math.random() * timerQuotes.length)]; | |
| } | |
| // ======== DRAWING VIA SIMULATED MOUSE EVENTS ======== | |
| // Lichess/Chessground uses right-click with modifier keys to draw: | |
| // - Green: Right-click (no modifiers) | |
| // - Red: Shift+Right-click OR Ctrl+Right-click | |
| // - Blue: Alt+Right-click OR Meta+Right-click | |
| // - Yellow: (Shift OR Ctrl) + (Alt OR Meta) + Right-click | |
| function getModifiersForBrush(brushKey) { | |
| // Returns { shiftKey, ctrlKey, altKey, metaKey } based on brush color | |
| // From chessground draw.ts eventBrush(): | |
| // brushes = ['green', 'red', 'blue', 'yellow'] | |
| // modA = (shift || ctrl) && rightClick -> index += 1 | |
| // modB = (alt || meta) -> index += 2 | |
| switch (brushKey) { | |
| case 'green': // index 0: no modifiers | |
| return { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }; | |
| case 'red': // index 1: need modA (shift or ctrl) | |
| return { shiftKey: true, ctrlKey: false, altKey: false, metaKey: false }; | |
| case 'blue': // index 2: need modB (alt or meta) | |
| return { shiftKey: false, ctrlKey: false, altKey: true, metaKey: false }; | |
| case 'yellow': // index 3: need modA + modB | |
| return { shiftKey: true, ctrlKey: false, altKey: true, metaKey: false }; | |
| default: | |
| return { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }; | |
| } | |
| } | |
| function getSquareCenterPos(squareKey) { | |
| // Get the center position of a square in client coordinates | |
| const board = document.querySelector('cg-board'); | |
| if (!board) return null; | |
| const rect = board.getBoundingClientRect(); | |
| const squareSize = rect.width / 8; | |
| const file = squareKey.charCodeAt(0) - 97; // a=0, h=7 | |
| const rank = parseInt(squareKey[1]) - 1; // 1=0, 8=7 | |
| // Check board orientation | |
| const isFlipped = document.querySelector('.cg-wrap.orientation-black') !== null; | |
| let col, row; | |
| if (isFlipped) { | |
| col = 7 - file; | |
| row = rank; | |
| } else { | |
| col = file; | |
| row = 7 - rank; | |
| } | |
| const x = rect.left + (col + 0.5) * squareSize; | |
| const y = rect.top + (row + 0.5) * squareSize; | |
| return { x: x, y: y }; | |
| } | |
| function simulateDrawing(origSquare, destSquare, brushKey) { | |
| // Simulate right-click drawing with appropriate modifier keys | |
| const board = document.querySelector('cg-board'); | |
| if (!board) { | |
| return; | |
| } | |
| const origPos = getSquareCenterPos(origSquare); | |
| const destPos = destSquare && destSquare !== origSquare ? getSquareCenterPos(destSquare) : origPos; | |
| if (!origPos || !destPos) { | |
| return; | |
| } | |
| const modifiers = getModifiersForBrush(brushKey); | |
| // Create and dispatch mousedown event (right-click = button 2) | |
| const mousedownEvent = new MouseEvent('mousedown', { | |
| bubbles: true, | |
| cancelable: true, | |
| view: window, | |
| button: 2, // Right mouse button | |
| buttons: 2, // Right button is pressed | |
| clientX: origPos.x, | |
| clientY: origPos.y, | |
| shiftKey: modifiers.shiftKey, | |
| ctrlKey: modifiers.ctrlKey, | |
| altKey: modifiers.altKey, | |
| metaKey: modifiers.metaKey | |
| }); | |
| board.dispatchEvent(mousedownEvent); | |
| // Small delay then dispatch mousemove if drawing arrow | |
| setTimeout(function() { | |
| if (destSquare && destSquare !== origSquare) { | |
| const mousemoveEvent = new MouseEvent('mousemove', { | |
| bubbles: true, | |
| cancelable: true, | |
| view: window, | |
| button: 2, | |
| buttons: 2, | |
| clientX: destPos.x, | |
| clientY: destPos.y, | |
| shiftKey: modifiers.shiftKey, | |
| ctrlKey: modifiers.ctrlKey, | |
| altKey: modifiers.altKey, | |
| metaKey: modifiers.metaKey | |
| }); | |
| board.dispatchEvent(mousemoveEvent); | |
| } | |
| // Dispatch mouseup to complete the drawing | |
| setTimeout(function() { | |
| const mouseupEvent = new MouseEvent('mouseup', { | |
| bubbles: true, | |
| cancelable: true, | |
| view: window, | |
| button: 2, | |
| buttons: 0, | |
| clientX: destPos.x, | |
| clientY: destPos.y, | |
| shiftKey: modifiers.shiftKey, | |
| ctrlKey: modifiers.ctrlKey, | |
| altKey: modifiers.altKey, | |
| metaKey: modifiers.metaKey | |
| }); | |
| board.dispatchEvent(mouseupEvent); | |
| }, 20); | |
| }, 20); | |
| } | |
| function getSquareFromPos(x, y) { | |
| // Calculate square from position | |
| const board = document.querySelector('cg-board'); | |
| if (!board) return null; | |
| const rect = board.getBoundingClientRect(); | |
| const squareSize = rect.width / 8; | |
| const col = Math.floor((x - rect.left) / squareSize); | |
| const row = Math.floor((y - rect.top) / squareSize); | |
| if (col < 0 || col > 7 || row < 0 || row > 7) return null; | |
| // Check board orientation | |
| const isFlipped = document.querySelector('.cg-wrap.orientation-black') !== null; | |
| const file = isFlipped ? String.fromCharCode(104 - col) : String.fromCharCode(97 + col); | |
| const rank = isFlipped ? (row + 1) : (8 - row); | |
| return file + rank; | |
| } | |
| function clearAllDrawings() { | |
| // Simulate pressing Escape or use the native clear | |
| // First try to access chessground | |
| if (window.lichess && window.lichess.round && window.lichess.round.chessground) { | |
| const cg = window.lichess.round.chessground; | |
| if (cg.state && cg.state.drawable) { | |
| cg.state.drawable.shapes = []; | |
| if (cg.state.drawable.onChange) { | |
| cg.state.drawable.onChange([]); | |
| } | |
| if (cg.redrawAll) { | |
| cg.redrawAll(); | |
| } | |
| return; | |
| } | |
| } | |
| // Fallback: Clear by clicking on empty square (this clears drawings in Lichess) | |
| const board = document.querySelector('cg-board'); | |
| if (board) { | |
| const rect = board.getBoundingClientRect(); | |
| // Click on center of board | |
| const clickEvent = new MouseEvent('mousedown', { | |
| bubbles: true, | |
| cancelable: true, | |
| view: window, | |
| button: 0, | |
| clientX: rect.left + rect.width / 2, | |
| clientY: rect.top + rect.height / 2 | |
| }); | |
| board.dispatchEvent(clickEvent); | |
| const upEvent = new MouseEvent('mouseup', { | |
| bubbles: true, | |
| cancelable: true, | |
| view: window, | |
| button: 0, | |
| clientX: rect.left + rect.width / 2, | |
| clientY: rect.top + rect.height / 2 | |
| }); | |
| board.dispatchEvent(upEvent); | |
| } | |
| } | |
| // ======== DRAWING TOOLBAR ======== | |
| function createDrawingToolbar() { | |
| const toolbar = document.createElement('div'); | |
| toolbar.id = 'vm-drawing-toolbar'; | |
| toolbar.style.cssText = 'width: 100%; margin-top: 8px; padding: 8px; background: rgba(100, 116, 139, 0.15); border: 1px solid #475569; border-radius: 6px; box-sizing: border-box;'; | |
| // Color swatches left, action buttons (draw/clear) on the right | |
| const swatchRow = document.createElement('div'); | |
| swatchRow.style.cssText = 'display: flex; align-items: center; justify-content: space-between; gap: 8px;'; | |
| // Left: swatches container | |
| const swatchesLeft = document.createElement('div'); | |
| swatchesLeft.style.cssText = 'display: flex; flex-wrap: wrap; gap: 6px; align-items: center;'; | |
| DRAWING_BRUSHES.forEach(function(brush, index) { | |
| const swatch = document.createElement('div'); | |
| swatch.className = 'vm-color-swatch'; | |
| swatch.setAttribute('data-brush', brush.key); | |
| swatch.style.cssText = 'width: 28px; height: 28px; border-radius: 4px; cursor: pointer; border: 2px solid transparent; transition: all 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.3);'; | |
| swatch.style.backgroundColor = brush.color; | |
| // Highlight selected brush | |
| if (brush.key === selectedBrush.key) { | |
| swatch.style.borderColor = '#ffffff'; | |
| swatch.style.transform = 'scale(1.15)'; | |
| } | |
| swatch.title = brush.name; | |
| swatch.onmouseover = function() { | |
| if (brush.key !== selectedBrush.key) { | |
| swatch.style.transform = 'scale(1.1)'; | |
| swatch.style.borderColor = 'rgba(255,255,255,0.5)'; | |
| } | |
| }; | |
| swatch.onmouseout = function() { | |
| if (brush.key !== selectedBrush.key) { | |
| swatch.style.transform = 'scale(1)'; | |
| swatch.style.borderColor = 'transparent'; | |
| } | |
| }; | |
| swatch.onclick = function() { | |
| selectBrush(brush, swatchesLeft); | |
| }; | |
| swatchesLeft.appendChild(swatch); | |
| }); | |
| // Right: action buttons container | |
| const actionsRight = document.createElement('div'); | |
| actionsRight.style.cssText = 'display: flex; gap: 6px; align-items: center;'; | |
| // Drawing mode toggle button (icon only) | |
| const drawBtn = document.createElement('button'); | |
| drawBtn.id = 'vm-draw-mode-btn'; | |
| drawBtn.textContent = '✏️'; | |
| drawBtn.title = 'Enable Drawing'; | |
| drawBtn.style.cssText = 'width: 32px; height: 28px; background: #475569; border: none; color: #e2e8f0; font-size: 16px; border-radius: 4px; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: center;'; | |
| drawBtn.onmouseover = function() { if (!drawingMode) drawBtn.style.background = '#334155'; }; | |
| drawBtn.onmouseout = function() { if (!drawingMode) drawBtn.style.background = '#475569'; }; | |
| drawBtn.onclick = function() { toggleDrawingMode(drawBtn); }; | |
| actionsRight.appendChild(drawBtn); | |
| // Clear button (icon only) | |
| const clearBtn = document.createElement('button'); | |
| clearBtn.textContent = '🗑️'; | |
| clearBtn.title = 'Clear Drawings'; | |
| clearBtn.style.cssText = 'width: 32px; height: 28px; background: #475569; border: none; color: #e2e8f0; font-size: 16px; border-radius: 4px; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: center;'; | |
| clearBtn.onmouseover = function() { clearBtn.style.background = '#334155'; }; | |
| clearBtn.onmouseout = function() { clearBtn.style.background = '#475569'; }; | |
| clearBtn.onclick = function() { clearAllDrawings(); }; | |
| actionsRight.appendChild(clearBtn); | |
| swatchRow.appendChild(swatchesLeft); | |
| swatchRow.appendChild(actionsRight); | |
| toolbar.appendChild(swatchRow); | |
| return toolbar; | |
| } | |
| function toggleDrawingMode(btn) { | |
| drawingMode = !drawingMode; | |
| if (drawingMode) { | |
| btn.textContent = '✏️'; | |
| btn.title = 'Drawing ON - Click board!'; | |
| btn.style.background = '#16a34a'; | |
| btn.style.boxShadow = '0 0 8px #16a34a'; | |
| setupBoardDrawing(); | |
| } else { | |
| btn.textContent = '✏️'; | |
| btn.title = 'Enable Drawing'; | |
| btn.style.background = '#475569'; | |
| btn.style.boxShadow = 'none'; | |
| removeBoardDrawing(); | |
| } | |
| } | |
| function setupBoardDrawing() { | |
| const board = document.querySelector('cg-board'); | |
| if (!board) { | |
| return; | |
| } | |
| // Use capture phase to intercept before Lichess handlers | |
| board.addEventListener('mousedown', handleDrawStart, true); | |
| board.addEventListener('mouseup', handleDrawEnd, true); | |
| board.addEventListener('mouseleave', handleDrawCancel, true); | |
| // Also handle touch for tablets | |
| board.addEventListener('touchstart', handleTouchStart, true); | |
| board.addEventListener('touchend', handleTouchEnd, true); | |
| board.style.cursor = 'crosshair'; | |
| } | |
| function removeBoardDrawing() { | |
| const board = document.querySelector('cg-board'); | |
| if (!board) return; | |
| board.removeEventListener('mousedown', handleDrawStart, true); | |
| board.removeEventListener('mouseup', handleDrawEnd, true); | |
| board.removeEventListener('mouseleave', handleDrawCancel, true); | |
| board.removeEventListener('touchstart', handleTouchStart, true); | |
| board.removeEventListener('touchend', handleTouchEnd, true); | |
| board.style.cursor = ''; | |
| drawStartPos = null; | |
| } | |
| function handleTouchStart(e) { | |
| if (!drawingMode) return; | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| e.stopImmediatePropagation(); | |
| const touch = e.touches[0]; | |
| drawStartPos = { | |
| square: getSquareFromPos(touch.clientX, touch.clientY), | |
| x: touch.clientX, | |
| y: touch.clientY | |
| }; | |
| } | |
| function handleTouchEnd(e) { | |
| if (!drawingMode || !drawStartPos) return; | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| e.stopImmediatePropagation(); | |
| const touch = e.changedTouches[0]; | |
| const endSquare = getSquareFromPos(touch.clientX, touch.clientY); | |
| if (drawStartPos.square && endSquare) { | |
| simulateDrawing(drawStartPos.square, endSquare, selectedBrush.key); | |
| } | |
| drawStartPos = null; | |
| } | |
| function handleDrawStart(e) { | |
| if (!drawingMode) return; | |
| if (e.button !== 0) return; // Left click only | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| e.stopImmediatePropagation(); | |
| drawStartPos = { | |
| square: getSquareFromPos(e.clientX, e.clientY), | |
| x: e.clientX, | |
| y: e.clientY | |
| }; | |
| } | |
| function handleDrawEnd(e) { | |
| if (!drawingMode || !drawStartPos) return; | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| e.stopImmediatePropagation(); | |
| const endSquare = getSquareFromPos(e.clientX, e.clientY); | |
| if (drawStartPos.square && endSquare) { | |
| simulateDrawing(drawStartPos.square, endSquare, selectedBrush.key); | |
| } | |
| drawStartPos = null; | |
| } | |
| function handleDrawCancel(e) { | |
| // Cancel drawing if mouse leaves board | |
| drawStartPos = null; | |
| } | |
| function selectBrush(brush, container) { | |
| selectedBrush = brush; | |
| // Update all swatches | |
| const swatches = container.querySelectorAll('.vm-color-swatch'); | |
| swatches.forEach(function(swatch) { | |
| if (swatch.getAttribute('data-brush') === brush.key) { | |
| swatch.style.borderColor = '#ffffff'; | |
| swatch.style.transform = 'scale(1.15)'; | |
| } else { | |
| swatch.style.borderColor = 'transparent'; | |
| swatch.style.transform = 'scale(1)'; | |
| } | |
| }); | |
| } | |
| // ======== PRINCIPLES AUTO-ADVANCE ======== | |
| function startPrincipleAutoAdvance() { | |
| // clear existing | |
| if (principleInterval) clearInterval(principleInterval); | |
| principleSecondsLeft = PRINCIPLE_AUTO_SECONDS; | |
| const timerEl = document.getElementById('vm-principle-timer'); | |
| const principleTextEl = document.getElementById('vm-principle-text'); | |
| if (timerEl) timerEl.textContent = 'Next in: ' + principleSecondsLeft + 's'; | |
| principleInterval = setInterval(() => { | |
| principleSecondsLeft--; | |
| if (timerEl) timerEl.textContent = 'Next in: ' + principleSecondsLeft + 's'; | |
| if (principleSecondsLeft <= 0) { | |
| currentPrinciple = (currentPrinciple + 1) % principles.length; | |
| if (principleTextEl) principleTextEl.textContent = principles[currentPrinciple]; | |
| principleSecondsLeft = PRINCIPLE_AUTO_SECONDS; | |
| if (timerEl) timerEl.textContent = 'Next in: ' + principleSecondsLeft + 's'; | |
| } | |
| }, 1000); | |
| } | |
| function createTopBar() { | |
| // Remove existing bar if present | |
| const existing = document.getElementById('vm-top-bar'); | |
| if (existing) existing.remove(); | |
| const bar = document.createElement('div'); | |
| bar.id = 'vm-top-bar'; | |
| bar.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; background: #1e1e2f; color: #f0f0f0; font-family: system-ui, sans-serif; font-size: 14px; display: flex; justify-content: space-between; align-items: center; padding: 6px 12px; z-index: 9999; box-shadow: 0 2px 6px rgba(0,0,0,0.3);'; | |
| // ---- Question section ---- | |
| const qDiv = document.createElement('div'); | |
| qDiv.id = 'vm-question'; | |
| qDiv.innerHTML = questions[currentQuestion].text; | |
| const doneBtn = document.createElement('button'); | |
| doneBtn.textContent = 'Done'; | |
| doneBtn.style.cssText = 'margin-left: 8px; background: #4ade80; border: none; color: #064e3b; font-weight: bold; padding: 2px 8px; border-radius: 4px; cursor: pointer;'; | |
| doneBtn.onclick = () => { | |
| currentQuestion = (currentQuestion + 1) % questions.length; | |
| qDiv.innerHTML = questions[currentQuestion].text; | |
| qDiv.appendChild(doneBtn); // Reattach button | |
| }; | |
| qDiv.appendChild(doneBtn); | |
| // ---- Timer section ---- | |
| const tDiv = document.createElement('div'); | |
| tDiv.id = 'vm-timer'; | |
| tDiv.textContent = timerRemaining + 's ⏳'; | |
| // ---- Quote section ---- | |
| const quoteDiv = document.createElement('div'); | |
| quoteDiv.id = 'vm-quote'; | |
| quoteDiv.textContent = randomQuote(); | |
| quoteDiv.style.fontStyle = 'italic'; | |
| quoteDiv.style.opacity = 0.8; | |
| bar.appendChild(qDiv); | |
| bar.appendChild(tDiv); | |
| bar.appendChild(quoteDiv); | |
| if (document.body) { | |
| document.body.appendChild(bar); | |
| startTimer(tDiv); | |
| updateStreak(streakDiv); | |
| } | |
| } | |
| // ======== TIMER LOGIC ======== | |
| function startTimer(tDiv) { | |
| if (timerInterval) clearInterval(timerInterval); | |
| timerElapsed = 0; | |
| tDiv.textContent = '0s ⏳'; | |
| tDiv.style.color = '#ef4444'; // Start red | |
| timerInterval = setInterval(() => { | |
| timerElapsed++; | |
| tDiv.textContent = timerElapsed + 's ⏳'; | |
| // Color coding: red (0-10s), yellow (10-15s), green (15s+) | |
| if (timerElapsed < 10) { | |
| tDiv.style.color = '#ef4444'; // Red | |
| } else if (timerElapsed < 15) { | |
| tDiv.style.color = '#fde047'; // Yellow | |
| } else { | |
| tDiv.style.color = '#10b981'; // Green | |
| } | |
| }, 1000); | |
| } | |
| // ======== STREAK LOGIC (REMOVED) ======== | |
| // Streak tracking has been removed from this version | |
| // ======== DETECT MOVE CHANGE & RESET ======== | |
| function checkMoveChange() { | |
| // Check for active move element (game page: kwdb.a1t) | |
| const activeMove = document.querySelector('kwdb.a1t'); | |
| if (!activeMove) return false; | |
| // Use the 'p' attribute or class as the unique identifier for each move | |
| const moveAttr = activeMove.getAttribute('p') || activeMove.className || ''; | |
| const moveText = activeMove.textContent.trim(); | |
| const currentMove = moveAttr + moveText; | |
| if (currentMove && currentMove !== lastActiveMove) { | |
| lastActiveMove = currentMove; | |
| return true; | |
| } | |
| return false; | |
| } | |
| function resetQuizState() { | |
| currentQuestion = 0; | |
| // Reset question text and buttons | |
| const qText = document.querySelector('#vm-question span'); | |
| const backBtn = document.getElementById('vm-back-btn'); | |
| const doneBtn = document.getElementById('vm-done-btn'); | |
| if (qText) { | |
| qText.innerHTML = questions[currentQuestion].text; | |
| } | |
| if (backBtn) { | |
| backBtn.textContent = '← Back'; | |
| } | |
| if (doneBtn) { | |
| doneBtn.style.display = ''; | |
| doneBtn.textContent = 'Got it'; | |
| } | |
| // Reset timer | |
| const tDiv = document.getElementById('vm-timer'); | |
| if (tDiv) { | |
| tDiv.style.color = '#ef4444'; // Red at start | |
| startTimer(tDiv); | |
| } | |
| // Reset quote | |
| const quoteDiv = document.getElementById('vm-quote'); | |
| if (quoteDiv) { | |
| quoteDiv.textContent = randomQuote(); | |
| } | |
| } | |
| // ======== INJECT CUSTOM SIDEBAR DIV ======== | |
| function injectCustomSidebarDiv() { | |
| // Check if already injected | |
| if (document.getElementById('vm-custom-sidebar')) { | |
| // Check for move change and reset if needed | |
| if (checkMoveChange()) { | |
| resetQuizState(); | |
| } | |
| return; | |
| } | |
| // Try to find an anchor element for game page | |
| const gameMeta = document.querySelector('.game__meta'); | |
| if (!gameMeta) { | |
| return; | |
| } | |
| // Create custom container with unique class | |
| const customDiv = document.createElement('div'); | |
| customDiv.id = 'vm-custom-sidebar'; | |
| customDiv.className = 'vm-game-helper'; | |
| customDiv.style.cssText = 'width: 100%; margin-top: 16px; padding: 14px; background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.4); border: 1px solid #334155; box-sizing: border-box; position: relative; overflow: hidden;'; | |
| // Question section with custom styling - PROMINENT DESIGN | |
| const qDiv = document.createElement('div'); | |
| qDiv.id = 'vm-question'; | |
| qDiv.style.cssText = 'width: 100%; margin-bottom: 16px; padding: 18px; background: rgba(100, 116, 139, 0.15); border-radius: 10px; box-shadow: 0 6px 20px rgba(148, 163, 184, 0.3); border: 3px solid #94a3b8; box-sizing: border-box;'; | |
| // Question text div | |
| const qTextDiv = document.createElement('div'); | |
| qTextDiv.style.cssText = 'width: 100%; font-weight: bold; font-size: ' + FONT_SIZE_QUESTION + '; line-height: 1.8; min-height: 80px; margin-bottom: 16px; color: #f1f5f9; text-shadow: 2px 2px 4px rgba(0,0,0,0.7);'; | |
| const qText = document.createElement('span'); | |
| qText.innerHTML = questions[currentQuestion].text; | |
| qTextDiv.appendChild(qText); | |
| // Buttons div | |
| const btnContainer = document.createElement('div'); | |
| btnContainer.style.cssText = 'width: 100%; display: flex; justify-content: space-between; gap: 12px; height: 40px;'; | |
| const backBtn = document.createElement('button'); | |
| backBtn.id = 'vm-back-btn'; | |
| backBtn.textContent = '← Back'; | |
| backBtn.style.cssText = 'background: #1e293b; border: 2px solid #ffffff; color: #ffffff; font-weight: bold; padding: 8px 16px; border-radius: 8px; cursor: pointer; font-size: ' + FONT_SIZE_BUTTON_LARGE + '; transition: all 0.2s; box-shadow: 0 3px 8px rgba(0,0,0,0.3);'; | |
| // Hide back button on first question | |
| if (currentQuestion === 0) { | |
| backBtn.style.visibility = 'hidden'; | |
| } | |
| backBtn.onmouseover = () => { backBtn.style.background = '#334155'; backBtn.style.transform = 'scale(1.08)'; }; | |
| backBtn.onmouseout = () => { backBtn.style.background = '#1e293b'; backBtn.style.transform = 'scale(1)'; }; | |
| backBtn.onclick = () => { | |
| if (currentQuestion > 0) { | |
| currentQuestion--; | |
| qText.innerHTML = questions[currentQuestion].text; | |
| doneBtn.style.display = ''; | |
| // Hide back button if we're at first question | |
| if (currentQuestion === 0) { | |
| backBtn.style.visibility = 'hidden'; | |
| } | |
| } | |
| }; | |
| const doneBtn = document.createElement('button'); | |
| doneBtn.id = 'vm-done-btn'; | |
| doneBtn.textContent = 'Got it ✓'; | |
| doneBtn.style.cssText = 'background: #16a34a; border: 2px solid #ffffff; color: #ffffff; font-weight: bold; padding: 8px 20px; border-radius: 8px; cursor: pointer; font-size: ' + FONT_SIZE_BUTTON_LARGE + '; transition: all 0.2s; box-shadow: 0 3px 8px rgba(0,0,0,0.3);'; | |
| doneBtn.onmouseover = () => { doneBtn.style.background = '#15803d'; doneBtn.style.transform = 'scale(1.08)'; }; | |
| doneBtn.onmouseout = () => { doneBtn.style.background = '#16a34a'; doneBtn.style.transform = 'scale(1)'; }; | |
| doneBtn.onclick = () => { | |
| currentQuestion++; | |
| // Show back button since we're past first question | |
| backBtn.style.visibility = 'visible'; | |
| if (currentQuestion >= questions.length) { | |
| qText.innerHTML = '<strong style="color: #fef3c7; font-size: 24px; text-shadow: 3px 3px 6px rgba(0,0,0,0.6);">' + motivationalMessages[Math.floor(Math.random() * motivationalMessages.length)] + '</strong>'; | |
| doneBtn.style.display = 'none'; | |
| } else { | |
| qText.innerHTML = questions[currentQuestion].text; | |
| } | |
| }; | |
| btnContainer.appendChild(backBtn); | |
| btnContainer.appendChild(doneBtn); | |
| qDiv.appendChild(qTextDiv); | |
| qDiv.appendChild(btnContainer); | |
| // Principles section with Back/Next controls - SUBTLE DESIGN | |
| const principlesDiv = document.createElement('div'); | |
| principlesDiv.id = 'vm-principles'; | |
| principlesDiv.style.cssText = 'width: 100%; margin-bottom: 10px; padding: 8px; background: rgba(100, 116, 139, 0.15); border: 1px solid #475569; border-radius: 5px; color: #94a3b8; box-sizing: border-box; opacity: 0.85;'; | |
| const principleTextDiv = document.createElement('div'); | |
| principleTextDiv.style.cssText = 'width: 100%; font-weight: normal; font-size: ' + FONT_SIZE_PRINCIPLE + '; line-height: 1.4; min-height: 50px; margin-bottom: 6px; text-align: center;'; | |
| const principleText = document.createElement('span'); | |
| principleText.id = 'vm-principle-text'; | |
| principleText.textContent = principles[currentPrinciple]; | |
| principleTextDiv.appendChild(principleText); | |
| const principleBtnContainer = document.createElement('div'); | |
| principleBtnContainer.style.cssText = 'width: 100%; display: flex; justify-content: space-between; gap: 8px; height: 24px;'; | |
| const principleBackBtn = document.createElement('button'); | |
| principleBackBtn.id = 'vm-principle-back-btn'; | |
| principleBackBtn.textContent = '← Back'; | |
| principleBackBtn.style.cssText = 'background: #475569; border: none; color: #e2e8f0; font-weight: normal; padding: 3px 8px; border-radius: 4px; cursor: pointer; font-size: ' + FONT_SIZE_BUTTON_SMALL + '; transition: all 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.2);'; | |
| principleBackBtn.onmouseover = () => { principleBackBtn.style.background = '#334155'; principleBackBtn.style.transform = 'scale(1.03)'; }; | |
| principleBackBtn.onmouseout = () => { principleBackBtn.style.background = '#475569'; principleBackBtn.style.transform = 'scale(1)'; }; | |
| principleBackBtn.onclick = () => { | |
| // move back (wrap around) | |
| currentPrinciple = (currentPrinciple - 1 + principles.length) % principles.length; | |
| principleText.textContent = principles[currentPrinciple]; | |
| // reset auto-advance timer | |
| startPrincipleAutoAdvance(); | |
| }; | |
| // Timer for principles (visible under principle text) | |
| const principleTimerDiv = document.createElement('div'); | |
| principleTimerDiv.id = 'vm-principle-timer'; | |
| principleTimerDiv.style.cssText = 'width: 100%; text-align: center; font-size: ' + FONT_SIZE_PRINCIPLE_TIMER + '; margin-bottom: 6px; color: #64748b; opacity: 0.7;'; | |
| principleTimerDiv.textContent = 'Next in: ' + PRINCIPLE_AUTO_SECONDS + 's'; | |
| principlesDiv.appendChild(principleTimerDiv); | |
| const principleNextBtn = document.createElement('button'); | |
| principleNextBtn.id = 'vm-principle-next-btn'; | |
| principleNextBtn.textContent = 'Next →'; | |
| principleNextBtn.style.cssText = 'background: #475569; border: none; color: #e2e8f0; font-weight: normal; padding: 3px 8px; border-radius: 4px; cursor: pointer; font-size: ' + FONT_SIZE_BUTTON_SMALL + '; transition: all 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.2);'; | |
| principleNextBtn.onmouseover = () => { principleNextBtn.style.background = '#334155'; principleNextBtn.style.transform = 'scale(1.03)'; }; | |
| principleNextBtn.onmouseout = () => { principleNextBtn.style.background = '#475569'; principleNextBtn.style.transform = 'scale(1)'; }; | |
| principleNextBtn.onclick = () => { | |
| // move next (wrap around) | |
| currentPrinciple = (currentPrinciple + 1) % principles.length; | |
| principleText.textContent = principles[currentPrinciple]; | |
| // reset auto-advance timer | |
| startPrincipleAutoAdvance(); | |
| }; | |
| principleBtnContainer.appendChild(principleBackBtn); | |
| principleBtnContainer.appendChild(principleNextBtn); | |
| principlesDiv.appendChild(principleTextDiv); | |
| principlesDiv.appendChild(principleBtnContainer); | |
| // Timer section | |
| const tDiv = document.createElement('div'); | |
| tDiv.id = 'vm-timer'; | |
| tDiv.textContent = '0s ⏳'; | |
| tDiv.style.cssText = 'width: 100%; margin-bottom: 10px; font-size: ' + FONT_SIZE_TIMER + '; font-weight: bold; color: #ef4444; text-align: center; padding: 10px; background: rgba(0,0,0,0.3); border-radius: 6px; box-sizing: border-box; text-shadow: 0 2px 4px rgba(0,0,0,0.3);'; | |
| // Quote section | |
| const quoteDiv = document.createElement('div'); | |
| quoteDiv.id = 'vm-quote'; | |
| quoteDiv.textContent = randomQuote(); | |
| quoteDiv.style.cssText = 'width: 100%; font-style: italic; margin-bottom: 10px; font-size: ' + FONT_SIZE_QUOTE + '; font-weight: bold; color: #ffffff; text-align: center; box-sizing: border-box; text-shadow: 0 1px 3px rgba(0,0,0,0.5);'; | |
| customDiv.appendChild(tDiv); | |
| customDiv.appendChild(qDiv); | |
| customDiv.appendChild(principlesDiv); | |
| customDiv.appendChild(quoteDiv); | |
| // ======== DRAWING TOOLBAR ======== | |
| const drawingToolbar = createDrawingToolbar(); | |
| customDiv.appendChild(drawingToolbar); | |
| // Insert into the game page container | |
| if (gameMeta) { | |
| // Game page: append after game__meta | |
| gameMeta.parentNode.insertBefore(customDiv, gameMeta.nextSibling); | |
| } | |
| // Start timer | |
| // Start principles auto-advance and widget timer | |
| startPrincipleAutoAdvance(); | |
| startTimer(tDiv); | |
| } | |
| // ======== OBSERVER ======== | |
| const observer = new MutationObserver(() => { | |
| // Only inject sidebar, don't create top bar | |
| injectCustomSidebarDiv(); | |
| }); | |
| observer.observe(document.documentElement, { childList: true, subtree: true }); | |
| // Retry mechanism - check periodically for 10 seconds | |
| let retryCount = 0; | |
| const maxRetries = 50; // 50 * 200ms = 10 seconds | |
| const retryInterval = setInterval(() => { | |
| retryCount++; | |
| injectCustomSidebarDiv(); | |
| if (document.getElementById('vm-custom-sidebar') || retryCount >= maxRetries) { | |
| clearInterval(retryInterval); | |
| } | |
| }, 200); | |
| })(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment