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:
@@ -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
|
||||
};
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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--) {
|
||||
|
||||
Reference in New Issue
Block a user