// Rendering functions // Cache for rendered chunks let chunkCanvasCache = new Map(); function render() { // Clear the canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw animated sky background renderSky(); // Get visible chunks const visibleChunks = getVisibleChunks(); // Render each visible chunk for (const { chunkX, chunkY, isVisible } of visibleChunks) { // Skip rendering for chunks that are not visible if (!isVisible) continue; const key = getChunkKey(chunkX, chunkY); if (!chunks.has(key)) continue; // Calculate screen position of chunk const screenX = (chunkX * CHUNK_SIZE - worldOffsetX) * PIXEL_SIZE; const screenY = (chunkY * CHUNK_SIZE - worldOffsetY) * PIXEL_SIZE; // Check if we need to render this chunk const needsRender = dirtyChunks.has(key) || worldMoved || !chunkCanvasCache.has(key); // Always render the chunk if it's in the stone layer (for visibility) // or if it needs rendering if (needsRender) { renderChunkToCache(chunkX, chunkY, key); } // Draw the cached chunk to the main canvas const cachedCanvas = chunkCanvasCache.get(key); if (cachedCanvas) { ctx.drawImage(cachedCanvas, screenX, screenY); } // Draw chunk border in debug mode if (debugMode) { ctx.strokeStyle = '#ff0000'; ctx.lineWidth = 1; ctx.strokeRect( screenX, screenY, CHUNK_SIZE * PIXEL_SIZE, CHUNK_SIZE * PIXEL_SIZE ); // Draw chunk coordinates ctx.fillStyle = '#ffffff'; ctx.font = '12px Arial'; ctx.fillText(`${chunkX},${chunkY}`, screenX + 5, screenY + 15); } // Remove this chunk from the dirty list after rendering if (dirtyChunks.has(key)) { dirtyChunks.delete(key); } } // Reset world moved flag after rendering worldMoved = false; // Update cloud position animation only (not colors or shapes) skyAnimationTime += skyAnimationSpeed; if (skyAnimationTime > 1) skyAnimationTime -= 1; // Render physics objects renderPhysicsObjects(ctx, worldOffsetX, worldOffsetY); // Render entities if (typeof renderEntities === 'function') { renderEntities(ctx, worldOffsetX, worldOffsetY); } // Draw cursor position and update debug info if (currentMouseX !== undefined && currentMouseY !== undefined) { const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX; const worldY = Math.floor(currentMouseY / PIXEL_SIZE) + worldOffsetY; // Update coordinates display in debug mode if (debugMode) { document.getElementById('coords').textContent = `Chunk: ${Math.floor(worldOffsetX / CHUNK_SIZE)},${Math.floor(worldOffsetY / CHUNK_SIZE)} | ` + `Mouse: ${worldX},${worldY} | Offset: ${Math.floor(worldOffsetX)},${Math.floor(worldOffsetY)}`; // Draw cursor outline const cursorScreenX = (worldX - worldOffsetX) * PIXEL_SIZE; const cursorScreenY = (worldY - worldOffsetY) * PIXEL_SIZE; ctx.strokeStyle = '#00ff00'; ctx.lineWidth = 2; ctx.strokeRect( cursorScreenX - PIXEL_SIZE, cursorScreenY - PIXEL_SIZE, PIXEL_SIZE * 3, PIXEL_SIZE * 3 ); // Draw a dot at the exact mouse position ctx.fillStyle = '#ff0000'; ctx.beginPath(); ctx.arc(currentMouseX, currentMouseY, 3, 0, Math.PI * 2); ctx.fill(); } } } // Render the animated sky background function renderSky() { // Create a gradient with fixed colors (no animation of colors) const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); // Use fixed color positions for a brighter, static sky gradient.addColorStop(0, SKY_COLORS[0]); gradient.addColorStop(0.5, SKY_COLORS[1]); gradient.addColorStop(1, SKY_COLORS[2]); // Fill the background with the gradient ctx.fillStyle = gradient; ctx.fillRect(0, 0, canvas.width, canvas.height); // Add some clouds (made of pixel squares instead of circles) const cloudCount = 5; ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; for (let i = 0; i < cloudCount; i++) { // Use animation time to move clouds slowly across the sky // Only the position is animated, not the shape const cloudX = ((i * canvas.width / cloudCount) + (skyAnimationTime * canvas.width * 2)) % (canvas.width * 1.5) - canvas.width * 0.25; const cloudY = canvas.height * (0.1 + (i % 3) * 0.1); const cloudSize = 40 + (i % 3) * 25; // Overall cloud size // Use a deterministic pattern for each cloud based on its index // This ensures the cloud shape doesn't change every frame const seed = i * 1000; // Different seed for each cloud // Create a pixelated cloud shape // Main cloud body (center) const centerSize = Math.floor(cloudSize * 0.25); drawPixelSquare(cloudX, cloudY, centerSize); // Create a pixelated cloud pattern with squares // Use fixed positions based on cloud index instead of random values const squareCount = 24; // More squares for more detail for (let j = 0; j < squareCount; j++) { // Use deterministic values based on j and seed const angle = (j / squareCount) * Math.PI * 2; const distance = cloudSize * 0.4; const offsetX = Math.cos(angle) * distance; const offsetY = Math.sin(angle) * distance * 0.6; // Flatten vertically // Size is deterministic based on position in the cloud const squareSize = Math.floor(cloudSize * (0.15 + (((j * seed) % 100) / 1000))); drawPixelSquare(cloudX + offsetX, cloudY + offsetY, squareSize); } // Add some additional squares for extra fluffiness for (let j = 0; j < 12; j++) { // Use deterministic offsets based on j and seed const offsetX = ((((j * seed) % 200) / 100) - 1) * cloudSize * 0.8; const offsetY = ((((j * seed + 50) % 200) / 100) - 1) * cloudSize * 0.4; const squareSize = Math.floor(cloudSize * (0.1 + (((j * seed + 100) % 100) / 1000))); drawPixelSquare(cloudX + offsetX, cloudY + offsetY, squareSize); } } // Helper function to draw a pixelated square function drawPixelSquare(x, y, size) { // Round to nearest pixel to avoid sub-pixel rendering const pixelX = Math.round(x); const pixelY = Math.round(y); const pixelSize = Math.max(1, Math.round(size)); ctx.fillRect( pixelX - Math.floor(pixelSize/2), pixelY - Math.floor(pixelSize/2), pixelSize, pixelSize ); } // Add a subtle horizon line to separate sky from world const horizonGradient = ctx.createLinearGradient(0, canvas.height * 0.4, 0, canvas.height * 0.6); horizonGradient.addColorStop(0, 'rgba(255, 255, 255, 0)'); horizonGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)'); horizonGradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); ctx.fillStyle = horizonGradient; ctx.fillRect(0, canvas.height * 0.4, canvas.width, canvas.height * 0.2); } // Render a chunk to an offscreen canvas and cache it function renderChunkToCache(chunkX, chunkY, key) { const chunk = chunks.get(key); // Create a new canvas for this chunk if it doesn't exist if (!chunkCanvasCache.has(key)) { const chunkCanvas = document.createElement('canvas'); chunkCanvas.width = CHUNK_SIZE * PIXEL_SIZE; chunkCanvas.height = CHUNK_SIZE * PIXEL_SIZE; chunkCanvasCache.set(key, chunkCanvas); } const chunkCanvas = chunkCanvasCache.get(key); const chunkCtx = chunkCanvas.getContext('2d'); // Clear the chunk canvas chunkCtx.clearRect(0, 0, chunkCanvas.width, chunkCanvas.height); // Render each pixel in the chunk for (let y = 0; y < CHUNK_SIZE; y++) { for (let x = 0; x < CHUNK_SIZE; x++) { const index = y * CHUNK_SIZE + x; const type = chunk[index]; // Always render stone layer even if it's not directly visible if (type === EMPTY && chunkY !== 1) continue; // For the stone layer (chunkY = 1), render a faint background even for empty spaces if (type === EMPTY && chunkY === 1) { // Use a very faint gray for empty spaces in the stone layer chunkCtx.fillStyle = 'rgba(100, 100, 100, 0.2)'; chunkCtx.fillRect( x * PIXEL_SIZE, y * PIXEL_SIZE, PIXEL_SIZE, PIXEL_SIZE ); continue; } // Set color based on type if (type === SAND) { chunkCtx.fillStyle = SAND_COLOR; } else if (type === WATER) { // Get water color from metadata with variation const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y); const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0; chunkCtx.fillStyle = WATER_COLORS[colorIndex]; } else if (type === WALL) { chunkCtx.fillStyle = WALL_COLOR; } else if (type === DIRT) { // Get dirt color from metadata with variation const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y); const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0; chunkCtx.fillStyle = DIRT_COLORS[colorIndex]; } else if (type === STONE) { // Get stone color from metadata with variation const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y); const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0; chunkCtx.fillStyle = STONE_COLORS[colorIndex]; } else if (type === GRASS) { // Get grass color from metadata with variation const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y); const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0; chunkCtx.fillStyle = GRASS_COLORS[colorIndex]; } else if (type === WOOD) { // Get wood color from metadata with variation const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y); const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0; chunkCtx.fillStyle = WOOD_COLORS[colorIndex]; } else if (type === SEED) { chunkCtx.fillStyle = SEED_COLOR; } else if (type === GRASS_BLADE) { // Use the same color variation as grass const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y); const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0; chunkCtx.fillStyle = GRASS_COLORS[colorIndex]; } else if (type === FLOWER) { // Get flower color from metadata or use a default const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y); chunkCtx.fillStyle = metadata && metadata.color ? metadata.color : FLOWER_COLORS[0]; } else if (type === TREE_SEED) { chunkCtx.fillStyle = SEED_COLOR; } else if (type === LEAF) { // Get leaf color from metadata with variation const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y); const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0; chunkCtx.fillStyle = LEAF_COLORS[colorIndex]; } else if (type === FIRE) { // Get fire color from metadata const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y); const colorIndex = metadata ? metadata.colorIndex : 0; chunkCtx.fillStyle = FIRE_COLORS[colorIndex]; } else if (type === LAVA) { // Get lava color from metadata const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y); const colorIndex = metadata ? metadata.colorIndex : 0; chunkCtx.fillStyle = LAVA_COLORS[colorIndex]; } // Draw the pixel chunkCtx.fillRect( x * PIXEL_SIZE, y * PIXEL_SIZE, PIXEL_SIZE, PIXEL_SIZE ); } } }