From 8693c3bee268bee489b61a7cd0ef1fde03dc50e2 Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Tue, 4 Nov 2025 18:27:52 +0000 Subject: [PATCH] Add paragraph line transitions and fix death detection for all modes - Implemented line-by-line explosions for paragraph mode - Lines explode when completed and remaining lines grow/shift down smoothly - Fixed death detection: now properly calculates text bottom position for each mode - Death triggers when bottom of text (not center) reaches top of spaceship - Works correctly for words, sentences, and paragraphs (multi-line modes) --- src/types.ts | 2 + src/useBoard/useBoard.ts | 71 +++++++++++++++++++++++++---- src/useGameStatus/useGameStatus.ts | 72 ++++++++++++++++++++++++++---- 3 files changed, 128 insertions(+), 17 deletions(-) diff --git a/src/types.ts b/src/types.ts index 58308a0..8acdaf4 100755 --- a/src/types.ts +++ b/src/types.ts @@ -48,6 +48,8 @@ export type FallingLetter = { // For line transitions in sentences/paragraphs lineTransition?: boolean; lineTransitionTime?: number; + currentLineIndex?: number; // Which line (0-3) is currently being typed in paragraphs + completedLines?: number[]; // Track which lines have been completed for paragraphs // For ship tracking in sentences currentCharX?: number; // X position of current character to type }; diff --git a/src/useBoard/useBoard.ts b/src/useBoard/useBoard.ts index 460731d..376023b 100644 --- a/src/useBoard/useBoard.ts +++ b/src/useBoard/useBoard.ts @@ -368,23 +368,76 @@ export function useBoard() { lineStartIndices.push(totalChars); } - // Enhanced Star Wars perspective for 4 lines - // First typed line (index 0) at bottom, last typed line (index 3) at top - const perspectiveFactors = [0.6, 0.7, 0.85, 1.0]; // Reversed: smallest at top, largest at bottom + // Check if we're in a line transition + const isTransitioning = letter.lineTransition && letter.lineTransitionTime; + const completedLines = letter.completedLines || []; + const transitionProgress = isTransitioning + ? Math.min((performance.now() - letter.lineTransitionTime) / 500, 1) // 500ms animation + : 0; + + // Base perspective factors (will be modified during transitions) + const basePerspectiveFactors = [0.6, 0.7, 0.85, 1.0]; // Reversed: smallest at top, largest at bottom const lineSpacing = fontSize * 1.2; ctx.save(); - // Render each line with perspective + // Render each line with perspective and animations lines.forEach((line, lineIndex) => { + // Skip rendering completed lines that have exploded + if (completedLines.includes(lineIndex) && (!isTransitioning || transitionProgress > 0.6)) { + return; + } + // Calculate visual position: line 0 at bottom, line 3 at top const visualIndex = lines.length - 1 - lineIndex; - const lineY = letter.y + (lineSpacing * (visualIndex - 1.5)); - const lineFontSize = fontSize * perspectiveFactors[visualIndex]; - const lineOpacity = 1.0 - (visualIndex * 0.1); // Decreasing opacity for lines at top - // Convergence toward vanishing point (more for lines at top) - const convergence = 0.3 * (1 - perspectiveFactors[visualIndex]); + // During transition, adjust positions and sizes + let lineY = letter.y + (lineSpacing * (visualIndex - 1.5)); + let lineFontSize = fontSize * basePerspectiveFactors[visualIndex]; + let lineOpacity = 1.0 - (visualIndex * 0.1); + + // If this line just completed and is exploding + if (isTransitioning && completedLines[completedLines.length - 1] === lineIndex && transitionProgress < 0.6) { + // Explosion effect for the completed line + const explosionProgress = transitionProgress / 0.6; + const explosionScale = 1 + explosionProgress * 2; + const explosionOpacity = 1 - explosionProgress; + + ctx.save(); + ctx.translate(letterX, lineY); + ctx.scale(explosionScale, explosionScale); + ctx.globalAlpha = explosionOpacity; + ctx.fillStyle = "#ffff00"; + ctx.font = `bold ${lineFontSize}px monospace`; + const lineWidth = ctx.measureText(line).width; + ctx.fillText(line, -lineWidth / 2, 0); + ctx.restore(); + return; // Skip normal rendering for exploding line + } + + // For remaining lines during transition, grow and shift down + if (isTransitioning && !completedLines.includes(lineIndex)) { + const numCompletedLines = completedLines.length; + // Shift lines down to fill the gap + const newVisualIndex = visualIndex + numCompletedLines; + if (newVisualIndex < 4) { + // Animate to new position + const targetY = letter.y + (lineSpacing * (newVisualIndex - 1.5)); + lineY = lineY + (targetY - lineY) * transitionProgress; + + // Grow to new size + const targetSize = fontSize * basePerspectiveFactors[newVisualIndex]; + lineFontSize = lineFontSize + (targetSize - lineFontSize) * transitionProgress; + + // Adjust opacity + const targetOpacity = 1.0 - (newVisualIndex * 0.1); + lineOpacity = lineOpacity + (targetOpacity - lineOpacity) * transitionProgress; + } + } + + // Convergence toward vanishing point + const convergenceFactor = 1 - (lineFontSize / (fontSize * 1.0)); // Based on current size + const convergence = 0.3 * convergenceFactor; const lineCenterX = letterX + ((CANVAS_WIDTH / 2) - letterX) * convergence; ctx.font = `bold ${lineFontSize}px monospace`; diff --git a/src/useGameStatus/useGameStatus.ts b/src/useGameStatus/useGameStatus.ts index 85dd324..181fefd 100755 --- a/src/useGameStatus/useGameStatus.ts +++ b/src/useGameStatus/useGameStatus.ts @@ -14,7 +14,6 @@ const MARGIN_LEFT = 50; // Safe margin from left edge const MARGIN_RIGHT = 50; // Safe margin from right edge const CANVAS_WIDTH = 800; const SPACESHIP_Y = 600 - 40; // Position of the ship on canvas -const COLLISION_THRESHOLD = 30; // Distance to trigger collision export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: FallingLetter[], lasers: Laser[]) => void, gameMode: GameMode = 'letters') { const [gameStatus, setGameStatus] = useState("waiting"); @@ -71,11 +70,33 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall // Check for collisions with ship lettersRef.current.forEach(letter => { if (letter.state === 'normal') { - const letterY = letter.y; - const distanceFromShip = Math.abs(letterY - SPACESHIP_Y); + // Calculate the bottom of the text based on game mode + let textBottom = letter.y; + + if (gameModeRef.current === 'letters') { + // For letters, y is the center, so add half font size + textBottom = letter.y + 16; // Half of 32px font + } else if (gameModeRef.current === 'words') { + // For words, y is the center, so add half font size + textBottom = letter.y + 16; // Half of 32px font + } else if (gameModeRef.current === 'sentences') { + // For sentences, we have 2 lines with the bottom line at y + fontSize + textBottom = letter.y + 20; // Bottom of lower line (fontSize = 20 for sentences) + } else if (gameModeRef.current === 'paragraphs') { + // For paragraphs, we have 4 lines + // Line positions are at (1.5 - lineIndex) * lineSpacing from y + // Bottom line (index 0) is at y + (1.5 * lineSpacing) + const fontSize = 20; + const lineSpacing = fontSize * 1.2; + textBottom = letter.y + (1.5 * lineSpacing) + (fontSize / 2); + } + + // Check if bottom of text reaches top of ship + const distanceFromShip = SPACESHIP_Y - textBottom; const horizontalDistance = Math.abs(letter.x - playerXRef.current); - if (distanceFromShip < COLLISION_THRESHOLD && horizontalDistance < 30) { + // Death occurs when text bottom reaches ship top (distance <= 0) + if (distanceFromShip <= 0 && (gameModeRef.current === 'letters' ? horizontalDistance < 30 : true)) { // Collision detected letter.state = 'exploding'; letter.stateStartTime = currentTime; @@ -161,10 +182,45 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall const charIndex = (currentLetter.charIndex ?? 0) + 1; const fullText = currentLetter.fullText ?? currentLetter.letter; - // For sentences/paragraphs, check if we completed the first line - const isSentenceMode = gameModeRef.current === 'sentences' || gameModeRef.current === 'paragraphs'; - if (isSentenceMode && fullText.length > 20) { - // Find the midpoint where text breaks + // Handle line transitions for sentences and paragraphs + if (gameModeRef.current === 'paragraphs' && fullText.length > 20) { + // Calculate line breaks for 4-line paragraphs + const numLines = 4; + const charsPerLine = Math.ceil(fullText.length / numLines); + const lineBreakPoints: number[] = []; + + let startIdx = 0; + for (let i = 0; i < numLines - 1; i++) { + let endIdx = startIdx + charsPerLine; + // Find nearest space to break at + for (let j = endIdx; j >= endIdx - 10 && j >= startIdx; j--) { + if (fullText[j] === ' ') { + endIdx = j; + break; + } + } + lineBreakPoints.push(endIdx); + startIdx = endIdx + 1; + } + + // Check if we just completed a line + const currentLineIndex = currentLetter.currentLineIndex ?? 0; + if (currentLineIndex < lineBreakPoints.length && charIndex === lineBreakPoints[currentLineIndex] + 1) { + // Trigger line transition animation + currentLetter.lineTransition = true; + currentLetter.lineTransitionTime = performance.now(); + currentLetter.currentLineIndex = currentLineIndex + 1; + if (!currentLetter.completedLines) { + currentLetter.completedLines = []; + } + currentLetter.completedLines.push(currentLineIndex); + // Continue to next character + currentLetter.charIndex = charIndex; + currentLetter.letter = fullText[charIndex]; + return; + } + } else if (gameModeRef.current === 'sentences' && fullText.length > 20) { + // Original sentence logic (2 lines) const midpoint = Math.ceil(fullText.length / 2); let breakPoint = midpoint; for (let i = midpoint; i >= midpoint - 10 && i >= 0; i--) {