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)
This commit is contained in:
2025-11-04 18:27:52 +00:00
parent ca0f20c5ca
commit 8693c3bee2
3 changed files with 128 additions and 17 deletions
+2
View File
@@ -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
};
+62 -9
View File
@@ -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`;
+64 -8
View File
@@ -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<GameStatus>("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--) {