diff options
author | elioat <elioat@tilde.institute> | 2024-12-27 07:25:05 -0500 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2024-12-27 07:25:05 -0500 |
commit | 1ae1729728f0907ddb17e9303edad19aa2ff143c (patch) | |
tree | b7bb5082e9099890156d9e9c36866a9dc2b8ee99 /html/rogue | |
parent | f1268ab00394de6f751c58f825efbc905680b161 (diff) | |
download | tour-1ae1729728f0907ddb17e9303edad19aa2ff143c.tar.gz |
*
Diffstat (limited to 'html/rogue')
-rw-r--r-- | html/rogue/index.html | 19 | ||||
-rw-r--r-- | html/rogue/js/camera.js | 56 | ||||
-rw-r--r-- | html/rogue/js/config.js | 32 | ||||
-rw-r--r-- | html/rogue/js/game.js | 169 | ||||
-rw-r--r-- | html/rogue/js/input.js | 19 | ||||
-rw-r--r-- | html/rogue/js/player.js | 188 | ||||
-rw-r--r-- | html/rogue/js/renderer.js | 97 | ||||
-rw-r--r-- | html/rogue/js/rogue.js | 144 | ||||
-rw-r--r-- | html/rogue/js/world.js | 1054 |
9 files changed, 310 insertions, 1468 deletions
diff --git a/html/rogue/index.html b/html/rogue/index.html index ef37885..79aac67 100644 --- a/html/rogue/index.html +++ b/html/rogue/index.html @@ -1,10 +1,15 @@ <!DOCTYPE html> -<html> +<html lang="en"> <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Rogue</title> <style> - body { + body, html { margin: 0; + padding: 0; + width: 100%; + height: 100%; overflow: hidden; } canvas { @@ -14,13 +19,7 @@ </head> <body> <canvas id="gameCanvas"></canvas> - <!-- Load config first since other files depend on it --> - <script src="js/config.js"></script> - <script src="js/renderer.js"></script> - <script src="js/input.js"></script> - <script src="js/world.js"></script> <script src="js/player.js"></script> - <script src="js/camera.js"></script> - <script src="js/rogue.js"></script> + <script src="js/game.js"></script> </body> -</html> +</html> \ No newline at end of file diff --git a/html/rogue/js/camera.js b/html/rogue/js/camera.js deleted file mode 100644 index e5d5d14..0000000 --- a/html/rogue/js/camera.js +++ /dev/null @@ -1,56 +0,0 @@ -const createCamera = (x, y) => ({ - x, - y, - width: window.innerWidth, - height: window.innerHeight, - // Define the dead zone (the area where camera won't move) - deadZone: { - x: window.innerWidth * 0.3, // 30% of screen width - y: window.innerHeight * 0.3, // 30% of screen height - } -}); - -const updateCamera = (camera, target) => { - // Calculate the center point of the screen - const screenCenterX = camera.x + camera.width / 2; - const screenCenterY = camera.y + camera.height / 2; - - // Calculate the distance from the target to the screen center - const distanceX = target.x - screenCenterX; - const distanceY = target.y - screenCenterY; - - // Calculate the dead zone boundaries - const deadZoneLeft = -camera.deadZone.x / 2; - const deadZoneRight = camera.deadZone.x / 2; - const deadZoneTop = -camera.deadZone.y / 2; - const deadZoneBottom = camera.deadZone.y / 2; - - // Calculate new camera position with smooth following - let newX = camera.x; - let newY = camera.y; - - // Horizontal camera movement - if (distanceX < deadZoneLeft) { - newX += distanceX - deadZoneLeft; - } else if (distanceX > deadZoneRight) { - newX += distanceX - deadZoneRight; - } - - // Vertical camera movement - if (distanceY < deadZoneTop) { - newY += distanceY - deadZoneTop; - } else if (distanceY > deadZoneBottom) { - newY += distanceY - deadZoneBottom; - } - - // Add subtle smoothing to camera movement - const smoothing = 0.1; - newX = camera.x + (newX - camera.x) * smoothing; - newY = camera.y + (newY - camera.y) * smoothing; - - return { - ...camera, - x: newX, - y: newY - }; -}; diff --git a/html/rogue/js/config.js b/html/rogue/js/config.js deleted file mode 100644 index 77100e1..0000000 --- a/html/rogue/js/config.js +++ /dev/null @@ -1,32 +0,0 @@ -const CONFIG = { - player: { - width: 32, - height: 48, - speed: 5, - jumpForce: 12, - gravity: 0.5, - color: '#ff0000' - }, - world: { - groundHeight: 12, - wilderness: { - vegetation: { - grass: { - colors: ['#3a5', '#294'], - hatch: { - angle: Math.PI / 4, - variation: Math.PI / 6, - spacing: 4, - length: 8, - margin: 2 - } - } - } - } - }, - camera: { - width: window.innerWidth, - height: window.innerHeight, - followSpeed: 0.1 - } -}; \ No newline at end of file diff --git a/html/rogue/js/game.js b/html/rogue/js/game.js new file mode 100644 index 0000000..29b5037 --- /dev/null +++ b/html/rogue/js/game.js @@ -0,0 +1,169 @@ +// Constants for hex grid +const HEX_SIZE = 20; // Fixed hex size +const HEX_WIDTH = HEX_SIZE * 2; +const HEX_HEIGHT = Math.sqrt(3) * HEX_SIZE; +const GRID_SIZE = 100; // Number of hexes in each dimension + +// Game state +const state = { + canvas: null, + ctx: null, + camera: { x: 0, y: 0 }, // Camera position in pixel coordinates + hexGrid: [], // Will store hex grid data +}; + +// Initialize the game +function init() { + state.canvas = document.getElementById('gameCanvas'); + state.ctx = state.canvas.getContext('2d'); + + // Initialize player + player.init(); + + function resize() { + state.canvas.width = window.innerWidth; + state.canvas.height = window.innerHeight; + // Center camera on player initially + centerCameraOnHex(player.position); + } + + window.addEventListener('resize', resize); + resize(); + state.canvas.addEventListener('click', handleClick); + requestAnimationFrame(gameLoop); +} + +// Center camera on a specific hex +function centerCameraOnHex(hex) { + const pixelCoord = hexToPixel(hex); + state.camera.x = pixelCoord.x - state.canvas.width / 2; + state.camera.y = pixelCoord.y - state.canvas.height / 2; +} + +// Smoothly move camera to target position +function updateCamera() { + const currentPos = player.getCurrentPosition(); + const targetPixel = hexToPixel(currentPos); + const targetCameraX = targetPixel.x - state.canvas.width / 2; + const targetCameraY = targetPixel.y - state.canvas.height / 2; + + // Smooth camera movement + state.camera.x += (targetCameraX - state.camera.x) * 0.1; + state.camera.y += (targetCameraY - state.camera.y) * 0.1; +} + +// Convert hex coordinates to pixel coordinates +function hexToPixel(hex) { + const x = HEX_SIZE * (3/2 * hex.q); + const y = HEX_SIZE * (Math.sqrt(3)/2 * hex.q + Math.sqrt(3) * hex.r); + return {x, y}; +} + +// Convert pixel coordinates to hex coordinates (adjusted for camera position) +function pixelToHex(screenX, screenY) { + const x = screenX + state.camera.x; + const y = screenY + state.camera.y; + + const q = (2/3 * x) / HEX_SIZE; + const r = (-1/3 * x + Math.sqrt(3)/3 * y) / HEX_SIZE; + return hexRound(q, r); +} + +// Draw a single hex +function drawHex(ctx, x, y) { + // Adjust position for camera + const screenX = x - state.camera.x; + const screenY = y - state.camera.y; + + // Only draw if hex is visible on screen (with padding) + if (screenX < -HEX_WIDTH || screenX > state.canvas.width + HEX_WIDTH || + screenY < -HEX_HEIGHT || screenY > state.canvas.height + HEX_HEIGHT) { + return; + } + + ctx.beginPath(); + for (let i = 0; i < 6; i++) { + const angle = 2 * Math.PI / 6 * i; + const xPos = screenX + HEX_SIZE * Math.cos(angle); + const yPos = screenY + HEX_SIZE * Math.sin(angle); + if (i === 0) { + ctx.moveTo(xPos, yPos); + } else { + ctx.lineTo(xPos, yPos); + } + } + ctx.closePath(); + ctx.stroke(); +} + +// Calculate which hexes should be drawn +function calculateViewportHexes() { + const hexes = []; + const halfGrid = Math.floor(GRID_SIZE / 2); + + for (let r = -halfGrid; r < halfGrid; r++) { + for (let q = -halfGrid; q < halfGrid; q++) { + hexes.push({q, r}); + } + } + return hexes; +} + +// Main game loop +function gameLoop() { + state.ctx.clearRect(0, 0, state.canvas.width, state.canvas.height); + + // Update game state + player.update(); + updateCamera(); + + // Draw hex grid + const viewportHexes = calculateViewportHexes(); + viewportHexes.forEach(hex => { + const pixel = hexToPixel(hex); + drawHex(state.ctx, pixel.x, pixel.y); + }); + + // Draw player + player.draw(state.ctx, hexToPixel, state.camera, HEX_SIZE); + + requestAnimationFrame(gameLoop); +} + +// Handle click/tap with camera offset +function handleClick(event) { + const rect = state.canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + + const hexCoord = pixelToHex(x, y); + player.moveTo(hexCoord); +} + +// Round hex coordinates to nearest hex +function hexRound(q, r) { + let x = q; + let z = r; + let y = -x-z; + + let rx = Math.round(x); + let ry = Math.round(y); + let rz = Math.round(z); + + const x_diff = Math.abs(rx - x); + const y_diff = Math.abs(ry - y); + const z_diff = Math.abs(rz - z); + + if (x_diff > y_diff && x_diff > z_diff) { + rx = -ry-rz; + } else if (y_diff > z_diff) { + ry = -rx-rz; + } else { + rz = -rx-ry; + } + + return {q: rx, r: rz}; +} + +// Start the game +init(); \ No newline at end of file diff --git a/html/rogue/js/input.js b/html/rogue/js/input.js deleted file mode 100644 index 047321c..0000000 --- a/html/rogue/js/input.js +++ /dev/null @@ -1,19 +0,0 @@ -const setupInputHandlers = (canvas, gameState) => { - let mouseMoveThrottle; - - canvas.addEventListener('mousemove', (e) => { - if (!mouseMoveThrottle) { - mouseMoveThrottle = setTimeout(() => { - gameState.debug.mouseX = e.clientX + gameState.camera.x; - gameState.debug.mouseY = e.clientY + gameState.camera.y; - mouseMoveThrottle = null; - }, 16); - } - }); - - window.addEventListener('keydown', (e) => { - if (e.key === 'd') { - gameState.debug.enabled = !gameState.debug.enabled; - } - }); -}; \ No newline at end of file diff --git a/html/rogue/js/player.js b/html/rogue/js/player.js index 270b26f..5b4a875 100644 --- a/html/rogue/js/player.js +++ b/html/rogue/js/player.js @@ -1,64 +1,140 @@ -const createPlayer = (x, y) => ({ - x, - y, - width: 32, - height: 32, - velocityX: 0, - velocityY: 0, - speed: 300, - jumping: false -}); +// Player state and controls +const player = { + position: { q: 0, r: 0 }, // Current hex position + target: null, // Target hex to move to + path: [], // Array of hex coordinates to follow + movementProgress: 0, // Progress of current movement (0 to 1) + moveSpeed: 0.1, // Movement speed (0 to 1 per frame) + + // Initialize player + init() { + this.position = { q: 0, r: 0 }; + this.target = null; + this.path = []; + return this; + }, -const updatePlayer = (player, deltaTime) => { - const keys = getKeys(); - const seconds = deltaTime / 1000; + // Get neighbors that share an edge with the given hex + getEdgeNeighbors(hex) { + const directions = [ + {q: 1, r: 0}, // East + {q: 0, r: 1}, // Southeast + {q: -1, r: 1}, // Southwest + {q: -1, r: 0}, // West + {q: 0, r: -1}, // Northwest + {q: 1, r: -1} // Northeast + ]; + + return directions.map(dir => ({ + q: hex.q + dir.q, + r: hex.r + dir.r + })); + }, - let velocityX = 0; - let velocityY = player.velocityY; + // Find path from current position to target + findPath(targetHex) { + const start = this.position; + const goal = targetHex; + + // Simple breadth-first search + const queue = [[start]]; + const visited = new Set(); + const key = hex => `${hex.q},${hex.r}`; + visited.add(key(start)); + + while (queue.length > 0) { + const path = queue.shift(); + const current = path[path.length - 1]; + + if (current.q === goal.q && current.r === goal.r) { + return path; + } + + const neighbors = this.getEdgeNeighbors(current); + for (const neighbor of neighbors) { + const neighborKey = key(neighbor); + if (!visited.has(neighborKey)) { + visited.add(neighborKey); + queue.push([...path, neighbor]); + } + } + } + + return null; // No path found + }, - // Horizontal movement - if (keys.ArrowLeft) velocityX -= player.speed; - if (keys.ArrowRight) velocityX += player.speed; + // Start moving to a target hex + moveTo(targetHex) { + if (!this.target) { + const path = this.findPath(targetHex); + if (path) { + this.path = path.slice(1); // Remove starting position + if (this.path.length > 0) { + this.target = this.path.shift(); + this.movementProgress = 0; + } + } + } + }, - // Simple jumping (can be improved) - if (keys.ArrowUp && !player.jumping) { - velocityY = -500; - } - - // Apply gravity - velocityY += 980 * seconds; // 980 pixels/secondĀ² - - // Update position - const x = player.x + velocityX * seconds; - const y = player.y + velocityY * seconds; - - // Create updated player state - let updatedPlayer = { - ...player, - x, - y, - velocityX, - velocityY - }; + // Update player position + update() { + if (this.target) { + this.movementProgress += this.moveSpeed; + + if (this.movementProgress >= 1) { + // Movement to current target complete + this.position = this.target; + this.target = null; + this.movementProgress = 0; + + // If there are more points in the path, move to the next one + if (this.path.length > 0) { + this.target = this.path.shift(); + this.movementProgress = 0; + } + } + } + }, - // Handle collisions with the world - return handleWorldCollisions(updatedPlayer, window.gameState.world); -}; + // Get current interpolated position + getCurrentPosition() { + if (!this.target) { + return this.position; + } -const renderPlayer = (ctx, player) => { - ctx.fillStyle = '#f00'; - ctx.fillRect(player.x, player.y, player.width, player.height); -}; + // Interpolate between current position and target + return { + q: this.position.q + (this.target.q - this.position.q) * this.movementProgress, + r: this.position.r + (this.target.r - this.position.r) * this.movementProgress + }; + }, -// Key handling -const keys = {}; + // Draw the player + draw(ctx, hexToPixel, camera, HEX_SIZE) { + const currentPos = this.getCurrentPosition(); + const pixelPos = hexToPixel(currentPos); + const screenX = pixelPos.x - camera.x; + const screenY = pixelPos.y - camera.y; -window.addEventListener('keydown', (e) => { - keys[e.key] = true; -}); - -window.addEventListener('keyup', (e) => { - keys[e.key] = false; -}); - -const getKeys = () => ({...keys}); + ctx.fillStyle = 'red'; + ctx.beginPath(); + ctx.arc(screenX, screenY, HEX_SIZE/3, 0, Math.PI * 2); + ctx.fill(); + + // Optionally, draw the remaining path + if (this.path.length > 0) { + ctx.strokeStyle = 'rgba(255,0,0,0.3)'; + ctx.beginPath(); + let lastPos = this.target || this.position; + this.path.forEach(point => { + const from = hexToPixel(lastPos); + const to = hexToPixel(point); + ctx.moveTo(from.x - camera.x, from.y - camera.y); + ctx.lineTo(to.x - camera.x, to.y - camera.y); + lastPos = point; + }); + ctx.stroke(); + } + } +}; \ No newline at end of file diff --git a/html/rogue/js/renderer.js b/html/rogue/js/renderer.js deleted file mode 100644 index 617b4c7..0000000 --- a/html/rogue/js/renderer.js +++ /dev/null @@ -1,97 +0,0 @@ -// Rendering constants -const RENDER_CONSTANTS = { - VIEWPORT_BUFFER: 100, - SKY_COLORS: { - TOP: '#1a1a2e', - UPPER_MID: '#2d1b3d', - LOWER_MID: '#462639', - BOTTOM: '#1f1f2f' - }, - GROUND_COLOR: '#4a4', - DEBUG_FONT: '14px monospace', - DEBUG_COLOR: '#ffffff' -}; - -// Helper functions for rendering -const createSkyGradient = (ctx, groundY) => { - const gradient = ctx.createLinearGradient(0, 0, 0, groundY); - gradient.addColorStop(0, RENDER_CONSTANTS.SKY_COLORS.TOP); - gradient.addColorStop(0.4, RENDER_CONSTANTS.SKY_COLORS.UPPER_MID); - gradient.addColorStop(0.7, RENDER_CONSTANTS.SKY_COLORS.LOWER_MID); - gradient.addColorStop(1, RENDER_CONSTANTS.SKY_COLORS.BOTTOM); - return gradient; -}; - -const getViewBounds = (camera) => ({ - left: camera.x - RENDER_CONSTANTS.VIEWPORT_BUFFER, - right: camera.x + camera.width + RENDER_CONSTANTS.VIEWPORT_BUFFER, - top: camera.y - RENDER_CONSTANTS.VIEWPORT_BUFFER, - bottom: camera.y + camera.height + RENDER_CONSTANTS.VIEWPORT_BUFFER -}); - -const isInView = (x, viewBounds) => - x > viewBounds.left && x < viewBounds.right; - -// Layer rendering functions -const renderBackground = (ctx, state, groundY, viewBounds) => { - // Save the current transform - ctx.save(); - - // Reset transform for viewport-fixed sky and initial black fill - ctx.setTransform(1, 0, 0, 1, 0, 0); - - // Sky (fixed to viewport) - ctx.fillStyle = state.cachedGradient; - ctx.fillRect(0, 0, ctx.canvas.width, groundY); - - // Initial black fill for the bottom of the viewport - ctx.fillStyle = '#000000'; - ctx.fillRect(0, groundY, ctx.canvas.width, ctx.canvas.height - groundY + 1); - - // Restore transform for world-space rendering - ctx.restore(); - - // Additional black fill that follows the camera (extends far to left and right) - ctx.fillStyle = '#000000'; - ctx.fillRect( - viewBounds.left - 2000, - groundY, - viewBounds.right - viewBounds.left + 4000, - ctx.canvas.height - ); -}; - -const renderBackgroundObjects = (ctx, state, groundY, viewBounds) => { - state.world.backgroundTrees - .filter(tree => isInView(tree.x, viewBounds)) - .forEach(tree => renderTree(ctx, tree, groundY)); - - state.world.backgroundRocks - .filter(rock => isInView(rock.x, viewBounds)) - .forEach(rock => renderRock(ctx, rock, groundY)); - - state.world.backgroundGrass - .filter(grass => isInView(grass.x, viewBounds)) - .forEach(grass => renderGrass(ctx, grass, groundY, state.time)); -}; - -const renderForegroundObjects = (ctx, state, groundY, viewBounds) => { - state.world.foregroundTrees - .filter(tree => isInView(tree.x, viewBounds)) - .forEach(tree => renderTree(ctx, tree, groundY)); - - state.world.foregroundRocks - .filter(rock => isInView(rock.x, viewBounds)) - .forEach(rock => renderRock(ctx, rock, groundY)); - - state.world.foregroundGrass - .filter(grass => isInView(grass.x, viewBounds)) - .forEach(grass => renderGrass(ctx, grass, groundY, state.time)); -}; - -const renderDebugInfo = (ctx, state) => { - ctx.fillStyle = RENDER_CONSTANTS.DEBUG_COLOR; - ctx.font = RENDER_CONSTANTS.DEBUG_FONT; - const text = `x: ${Math.round(state.debug.mouseX)}, y: ${Math.round(state.debug.mouseY)}`; - ctx.fillText(text, 10, ctx.canvas.height - 20); -}; \ No newline at end of file diff --git a/html/rogue/js/rogue.js b/html/rogue/js/rogue.js deleted file mode 100644 index 64716a4..0000000 --- a/html/rogue/js/rogue.js +++ /dev/null @@ -1,144 +0,0 @@ -window.gameState = null; - -const initGame = () => { - const canvas = document.getElementById('gameCanvas'); - const ctx = canvas.getContext('2d'); - - // Target frame rate - const FPS = 60; - const FRAME_TIME = 1000 / FPS; - let lastFrameTime = 0; - - // Set canvas to full viewport size - const resizeCanvas = () => { - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - // Clear any cached gradients when resizing - if (window.gameState) { - window.gameState.cachedGradient = null; - } - }; - - window.addEventListener('resize', resizeCanvas); - resizeCanvas(); - - // Calculate initial player position at ground level - const groundY = canvas.height - CONFIG.world.groundHeight; - const initialPlayerY = groundY - CONFIG.player.height; - - // Game state - let gameState = { - time: 0, - player: createPlayer(100, initialPlayerY), - camera: createCamera(0, 0), - world: createWorld(), - debug: { - enabled: false, - mouseX: 0, - mouseY: 0 - } - }; - - // Make gameState globally accessible - window.gameState = gameState; - - // Set up input handlers - setupInputHandlers(canvas, gameState); - - // Game loop - const gameLoop = (timestamp) => { - // Check if enough time has passed since last frame - if (timestamp - lastFrameTime < FRAME_TIME) { - requestAnimationFrame(gameLoop); - return; - } - - // Calculate delta time (capped at 1 second to prevent huge jumps) - const deltaTime = Math.min(timestamp - lastFrameTime, 1000); - lastFrameTime = timestamp; - gameState.time = timestamp; - - // Clear the entire canvas - ctx.clearRect(0, 0, canvas.width, canvas.height); - - // Update - gameState = updateGame(gameState, deltaTime); - - // Render - render(ctx, gameState); - - // Schedule next frame - requestAnimationFrame(gameLoop); - }; - - // Start the game loop - lastFrameTime = performance.now(); - requestAnimationFrame(gameLoop); -}; - -const updateGame = (state, deltaTime) => { - const updatedPlayer = updatePlayer(state.player, deltaTime); - const updatedCamera = updateCamera(state.camera, updatedPlayer); - - return { - ...state, - player: updatedPlayer, - camera: updatedCamera - }; -}; - -const render = (ctx, state) => { - const groundY = ctx.canvas.height - state.world.groundHeight; - - // Cache sky gradient - if (!state.cachedGradient) { - state.cachedGradient = createSkyGradient(ctx, groundY); - } - - const viewBounds = getViewBounds(state.camera); - - // Apply camera transform - ctx.save(); - ctx.translate(-state.camera.x, -state.camera.y); - - // Clear the transformed canvas area - ctx.clearRect( - viewBounds.left, - 0, - viewBounds.right - viewBounds.left, - ctx.canvas.height - ); - - // Render layers - renderBackground(ctx, state, groundY, viewBounds); - renderBackgroundObjects(ctx, state, groundY, viewBounds); - - // Render platforms - state.world.platforms.forEach(platform => { - ctx.fillStyle = platform.color; - ctx.fillRect(platform.x, platform.y, platform.width, platform.height); - }); - - renderPlayer(ctx, state.player); - renderForegroundObjects(ctx, state, groundY, viewBounds); - - // Render ground line - ctx.fillStyle = RENDER_CONSTANTS.GROUND_COLOR; - ctx.fillRect( - state.camera.x - 1000, - groundY, - ctx.canvas.width + 2000, - 1 - ); - - // Handle debug rendering - if (state.debug.enabled) { - ctx.restore(); - renderDebugInfo(ctx, state); - } else { - ctx.restore(); - } -}; - -// Initialize the game when the window loads -window.addEventListener('load', initGame); diff --git a/html/rogue/js/world.js b/html/rogue/js/world.js deleted file mode 100644 index 2b2529a..0000000 --- a/html/rogue/js/world.js +++ /dev/null @@ -1,1054 +0,0 @@ -// Color palettes and configurations -const NATURE_COLORS = { - GREENS: [ - '#11aa33', // Darker forest green - '#22bb44', // Medium forest green - '#33cc55', // Bright forest green - '#118844', // Deep sea green - '#22aa66', // Sage green - '#11bb55', // Deep pine - '#229955', // Ocean green - '#33bb66', // Fresh spring green - '#117744', // Dark emerald - ], - TRUNK_COLORS: { - LIGHT: '#664422', - MEDIUM: '#553311', - DARK: '#442200' - } -}; - -const GRASS_CONFIG = { - MIN_BLADES: 4, - MAX_BLADES: 8, - SPREAD_FACTOR: 0.7, - RUSTLE_SPEED: 300, - WAVE_SPEED: 1000, - GLOW_SPEED: 500, - BASE_BLADE_WIDTH: 5 -}; - -const FIR_CONFIG = { - ROW_COUNT: 40, - FEATHERS_PER_ROW: 24, - EDGE_FEATHER_COUNT: 40, - FEATHER_WIDTH: 2, - TAPER_POWER: 0.8, - CENTER_NEEDLES: 5, // Number of needles in center column - NEEDLE_SPACING: 1.5, // Spacing between center needles - UPWARD_TILT: 0.1, // Amount of upward tilt (in radians) - ANGLE_VARIATION: 0.05, // Variation in needle angles - LENGTH_MULTIPLIER: 0.8 // Base length multiplier for needles -}; - -// Define world object types -const WORLD_OBJECTS = { - PLATFORM: 'platform', - FIR_BACKGROUND: 'fir_background', - FIR_FOREGROUND: 'fir_foreground', - MAPLE_BACKGROUND: 'maple_background', - MAPLE_FOREGROUND: 'maple_foreground', - ROCK_BACKGROUND: 'rock_background', - ROCK_FOREGROUND: 'rock_foreground', - GRASS_BACKGROUND: 'grass_background', - GRASS_FOREGROUND: 'grass_foreground' -}; - -// Separate configurations for different tree types -const FIR_TYPES = { - SMALL: { - type: 'fir', - width: 100, - height: 230, - canopyOffset: 45, - canopyColor: '#22aa44', - trunkColor: '#553311' - }, - LARGE: { - type: 'fir', - width: 150, - height: 320, - canopyOffset: 65, - canopyColor: '#11aa44', - trunkColor: '#442200' - } -}; - -const MAPLE_TYPES = { - SMALL: { - type: 'maple', - width: 100, - height: 330, - trunkHeight: 60, - canopyRadius: 35, - leafClusters: 5, - canopyColor: '#33aa22', // Bright green - trunkColor: '#664422' - }, - MEDIUM: { - type: 'maple', - width: 130, - height: 360, - trunkHeight: 70, - canopyRadius: 45, - leafClusters: 6, - canopyColor: '#228833', // Deep forest green - trunkColor: '#553311' - }, - LARGE: { - type: 'maple', - width: 160, - height: 400, - trunkHeight: 85, - canopyRadius: 55, - leafClusters: 7, - canopyColor: '#115522', // Dark green - trunkColor: '#553311' - }, - BRIGHT: { - type: 'maple', - width: 140, - height: 380, - trunkHeight: 75, - canopyRadius: 50, - leafClusters: 6, - canopyColor: '#44bb33', // Vibrant green - trunkColor: '#664422' - }, - SAGE: { - type: 'maple', - width: 150, - height: 490, - trunkHeight: 80, - canopyRadius: 52, - leafClusters: 6, - canopyColor: '#225544', // Sage green - trunkColor: '#553311' - } -}; - -// Rock configurations -const ROCK_TYPES = { - SMALL: { - width: 40, - height: 30, - color: '#666', - highlights: '#888' - }, - MEDIUM: { - width: 70, - height: 45, - color: '#555', - highlights: '#777' - }, - LARGE: { - width: 100, - height: 60, - color: '#444', - highlights: '#666' - } -}; - -// Add grass configurations -const GRASS_TYPES = { - TALL: { - type: 'grass', - width: 30, - height: 40, - color: '#33aa55', - shadowColor: '#229944' - }, - SHORT: { - type: 'grass', - width: 20, - height: 25, - color: '#33bb66', - shadowColor: '#22aa55' - }, - GOLDEN_TALL: { - type: 'grass', - width: 30, - height: 40, - color: '#eebb33', - shadowColor: '#cc9922' - }, - GOLDEN_SHORT: { - type: 'grass', - width: 20, - height: 25, - color: '#ffcc44', - shadowColor: '#ddaa33' - }, - BLUE_TALL: { - type: 'grass', - width: 30, - height: 40, - color: '#44aaff', - shadowColor: '#2299ff', - glowing: true - }, - BLUE_SHORT: { - type: 'grass', - width: 20, - height: 25, - color: '#55bbff', - shadowColor: '#33aaff', - glowing: true - } -}; - -// Utility functions -const utils = { - getRandomColorFromPalette: (palette, seed1, seed2) => { - const colorIndex = Math.floor(seededRandom(seed1, seed2) * palette.length); - return palette[colorIndex]; - }, - - getBladeCount: (x, height) => { - const randomValue = Math.abs(seededRandom(x, height)); - return GRASS_CONFIG.MIN_BLADES + - Math.round(randomValue * (GRASS_CONFIG.MAX_BLADES - GRASS_CONFIG.MIN_BLADES)); - }, - - calculateTaper: (t) => Math.pow(1 - t, FIR_CONFIG.TAPER_POWER) -}; - -// Create a world with platforms, trees, and rocks -const createWorld = () => { - const world = { - groundHeight: 12, - // Separate arrays for different layers - backgroundTrees: [ - // Far left trees - { - type: WORLD_OBJECTS.FIR_BACKGROUND, - x: -1500, - config: FIR_TYPES.LARGE - }, - { - type: WORLD_OBJECTS.MAPLE_BACKGROUND, - x: -1200, - config: MAPLE_TYPES.SAGE - }, - { - type: WORLD_OBJECTS.FIR_BACKGROUND, - x: -900, - config: FIR_TYPES.SMALL - }, - // Existing trees - { - type: WORLD_OBJECTS.FIR_BACKGROUND, - x: -400, - config: FIR_TYPES.LARGE - }, - { - type: WORLD_OBJECTS.MAPLE_BACKGROUND, - x: -250, - config: MAPLE_TYPES.BRIGHT - }, - { - type: WORLD_OBJECTS.FIR_BACKGROUND, - x: 50, - config: FIR_TYPES.LARGE - }, - { - type: WORLD_OBJECTS.MAPLE_BACKGROUND, - x: 250, - config: MAPLE_TYPES.MEDIUM - }, - { - type: WORLD_OBJECTS.FIR_BACKGROUND, - x: 500, - config: FIR_TYPES.SMALL - }, - { - type: WORLD_OBJECTS.MAPLE_BACKGROUND, - x: 650, - config: MAPLE_TYPES.SMALL - }, - { - type: WORLD_OBJECTS.FIR_BACKGROUND, - x: 900, - config: FIR_TYPES.LARGE - }, - { - type: WORLD_OBJECTS.MAPLE_BACKGROUND, - x: 1100, - config: MAPLE_TYPES.LARGE - }, - // Far right trees - { - type: WORLD_OBJECTS.MAPLE_BACKGROUND, - x: 1400, - config: MAPLE_TYPES.LARGE - }, - { - type: WORLD_OBJECTS.FIR_BACKGROUND, - x: 1700, - config: FIR_TYPES.SMALL - }, - { - type: WORLD_OBJECTS.MAPLE_BACKGROUND, - x: 2000, - config: MAPLE_TYPES.LARGE - } - ], - backgroundRocks: [ - // Far left rocks - { - type: WORLD_OBJECTS.ROCK_BACKGROUND, - x: -1300, - config: ROCK_TYPES.LARGE - }, - { - type: WORLD_OBJECTS.ROCK_BACKGROUND, - x: -1000, - config: ROCK_TYPES.SMALL - }, - { - type: WORLD_OBJECTS.ROCK_BACKGROUND, - x: -300, - config: ROCK_TYPES.MEDIUM - }, - { - type: WORLD_OBJECTS.ROCK_BACKGROUND, - x: -100, - config: ROCK_TYPES.SMALL - }, - { - type: WORLD_OBJECTS.ROCK_BACKGROUND, - x: 150, - config: ROCK_TYPES.LARGE - }, - { - type: WORLD_OBJECTS.ROCK_BACKGROUND, - x: 400, - config: ROCK_TYPES.SMALL - }, - { - type: WORLD_OBJECTS.ROCK_BACKGROUND, - x: 750, - config: ROCK_TYPES.MEDIUM - }, - { - type: WORLD_OBJECTS.ROCK_BACKGROUND, - x: 1000, - config: ROCK_TYPES.SMALL - }, - // Far right rocks - { - type: WORLD_OBJECTS.ROCK_BACKGROUND, - x: 1600, - config: ROCK_TYPES.MEDIUM - }, - { - type: WORLD_OBJECTS.ROCK_BACKGROUND, - x: 1900, - config: ROCK_TYPES.SMALL - } - ], - platforms: [ - // { - // type: WORLD_OBJECTS.PLATFORM, - // x: 300, - // y: 300, - // width: 200, - // height: 20, - // color: '#484' - // }, - // { - // type: WORLD_OBJECTS.PLATFORM, - // x: 600, - // y: 200, - // width: 200, - // height: 20, - // color: '#484' - // }, - // { - // type: WORLD_OBJECTS.PLATFORM, - // x: -200, - // y: 250, - // width: 200, - // height: 20, - // color: '#484' - // } - ], - foregroundTrees: [ - // Far left trees - { - type: WORLD_OBJECTS.FIR_FOREGROUND, - x: -1400, - config: FIR_TYPES.LARGE - }, - { - type: WORLD_OBJECTS.MAPLE_FOREGROUND, - x: -1100, - config: MAPLE_TYPES.SMALL - }, - // Existing trees - { - type: WORLD_OBJECTS.MAPLE_FOREGROUND, - x: -150, - config: MAPLE_TYPES.BRIGHT - }, - { - type: WORLD_OBJECTS.FIR_FOREGROUND, - x: 200, - config: FIR_TYPES.SMALL - }, - { - type: WORLD_OBJECTS.MAPLE_FOREGROUND, - x: 450, - config: MAPLE_TYPES.SAGE - }, - { - type: WORLD_OBJECTS.FIR_FOREGROUND, - x: 800, - config: FIR_TYPES.LARGE - }, - { - type: WORLD_OBJECTS.MAPLE_FOREGROUND, - x: 1200, - config: MAPLE_TYPES.SMALL - }, - // Far right trees - { - type: WORLD_OBJECTS.FIR_FOREGROUND, - x: 1500, - config: FIR_TYPES.LARGE - }, - { - type: WORLD_OBJECTS.MAPLE_FOREGROUND, - x: 1800, - config: MAPLE_TYPES.SMALL - } - ], - foregroundRocks: [ - // Far left rocks - { - type: WORLD_OBJECTS.ROCK_FOREGROUND, - x: -1000, - config: ROCK_TYPES.MEDIUM - }, - { - type: WORLD_OBJECTS.ROCK_FOREGROUND, - x: 0, - config: ROCK_TYPES.SMALL - }, - { - type: WORLD_OBJECTS.ROCK_FOREGROUND, - x: 300, - config: ROCK_TYPES.MEDIUM - }, - { - type: WORLD_OBJECTS.ROCK_FOREGROUND, - x: 700, - config: ROCK_TYPES.SMALL - }, - { - type: WORLD_OBJECTS.ROCK_FOREGROUND, - x: 950, - config: ROCK_TYPES.LARGE - }, - // Far right rocks - { - type: WORLD_OBJECTS.ROCK_FOREGROUND, - x: 1500, - config: ROCK_TYPES.LARGE - } - ], - backgroundGrass: [ - // Far left grass - { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: -1400, config: GRASS_TYPES.BLUE_TALL }, - { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: -1100, config: GRASS_TYPES.GOLDEN_SHORT }, - { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: -950, config: GRASS_TYPES.TALL }, - { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: -350, config: GRASS_TYPES.SHORT }, - { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: -180, config: GRASS_TYPES.GOLDEN_TALL }, - { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 100, config: GRASS_TYPES.TALL }, - { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 320, config: GRASS_TYPES.BLUE_SHORT }, - { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 580, config: GRASS_TYPES.GOLDEN_SHORT }, - { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 750, config: GRASS_TYPES.BLUE_TALL }, - { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 950, config: GRASS_TYPES.TALL }, - { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 1050, config: GRASS_TYPES.GOLDEN_TALL }, - // Far right grass - { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 1500, config: GRASS_TYPES.BLUE_SHORT }, - { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 1750, config: GRASS_TYPES.GOLDEN_TALL }, - { type: WORLD_OBJECTS.GRASS_BACKGROUND, x: 1850, config: GRASS_TYPES.SHORT } - ], - foregroundGrass: [ - // Far left grass - { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: -1250, config: GRASS_TYPES.TALL }, - { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: -1050, config: GRASS_TYPES.BLUE_SHORT }, - { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: -280, config: GRASS_TYPES.BLUE_TALL }, - { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: -50, config: GRASS_TYPES.GOLDEN_SHORT }, - { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 150, config: GRASS_TYPES.TALL }, - { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 420, config: GRASS_TYPES.BLUE_SHORT }, - { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 680, config: GRASS_TYPES.GOLDEN_TALL }, - { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 880, config: GRASS_TYPES.SHORT }, - { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 1150, config: GRASS_TYPES.BLUE_TALL }, - // Far right grass - { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 1650, config: GRASS_TYPES.GOLDEN_TALL }, - { type: WORLD_OBJECTS.GRASS_FOREGROUND, x: 1850, config: GRASS_TYPES.BLUE_SHORT } - ], - // Track grass animation states - grassStates: {}, - // Add methods for quick spatial lookups - getObjectsInView: function(bounds) { - return { - backgroundTrees: this.backgroundTrees.filter(tree => - tree.x > bounds.left && tree.x < bounds.right - ), - // ... similar for other object types - }; - } - }; - - return world; -}; - -// Add seededRandom function -const seededRandom = (x, y) => { - const dot = x * 12.9898 + y * 78.233; - return (Math.sin(dot) * 43758.5453123) % 1; -}; - -// Add hatching helper function -const addHatchingPattern = (ctx, x, y, width, height, color) => { - const spacing = 4; // Space between hatch lines - const length = 10; // Length of each hatch line - const margin = 4; // Increased margin from edges - const baseAngle = Math.PI * 1.5; // Vertical angle (pointing down) - const angleVariation = Math.PI / 12; // Reduced angle variation - - // Define darker brown shades - const brownShades = [ - '#442211', // Dark brown - '#553322', // Medium dark brown - '#443311', // Reddish dark brown - '#332211', // Very dark brown - '#554422', // Warm dark brown - ]; - - // Create clipping path for trunk - ctx.save(); - ctx.beginPath(); - ctx.rect( - x - width/2, - y - height, - width, - height - ); - ctx.clip(); - - // Calculate bounds with increased safety margin - const bounds = { - minX: x - width/2 + margin, - maxX: x + width/2 - margin, - minY: y - height + margin, - maxY: y - margin - }; - - // Draw hatching - ctx.lineWidth = 1; - for (let hatchX = bounds.minX; hatchX < bounds.maxX; hatchX += spacing) { - for (let hatchY = bounds.minY; hatchY < bounds.maxY; hatchY += spacing) { - // Use position for random variation - const seed1 = hatchX * 13.37; - const seed2 = hatchY * 7.89; - - // Random variations with reduced range - const variation = { - x: (seededRandom(seed1, seed2) - 0.5) * 1.5, // Reduced variation - y: (seededRandom(seed2, seed1) - 0.5) * 1.5, // Reduced variation - angle: baseAngle + (seededRandom(seed1 + seed2, seed2 - seed1) - 0.5) * angleVariation - }; - - // Pick a random brown shade - const colorIndex = Math.floor(seededRandom(seed1 * seed2, seed2 * seed1) * brownShades.length); - ctx.strokeStyle = brownShades[colorIndex]; - - // Draw hatch line - ctx.beginPath(); - ctx.moveTo( - hatchX + variation.x, - hatchY + variation.y - ); - ctx.lineTo( - hatchX + Math.cos(variation.angle) * length + variation.x, - hatchY + Math.sin(variation.angle) * length + variation.y - ); - ctx.stroke(); - } - } - - ctx.restore(); -}; - -class FirTreeRenderer { - static renderTrunk(ctx, x, width, height, groundY, trunkColor) { - const trunkWidth = width/4; - const trunkHeight = height; - - ctx.fillStyle = trunkColor; - ctx.fillRect( - x - trunkWidth/2, - groundY - trunkHeight, - trunkWidth, - trunkHeight - ); - - addHatchingPattern(ctx, x, groundY, trunkWidth, trunkHeight, trunkColor); - } - - static renderCenterNeedles(ctx, x, rowY, taper, featherLength, greenColors) { - for (let i = -FIR_CONFIG.CENTER_NEEDLES; i <= FIR_CONFIG.CENTER_NEEDLES; i++) { - const offset = i * (FIR_CONFIG.NEEDLE_SPACING * 1.5); - - // Left needle - FirTreeRenderer.renderSingleNeedle( - ctx, x + offset, rowY, Math.PI, taper, featherLength, greenColors - ); - - // Right needle - FirTreeRenderer.renderSingleNeedle( - ctx, x + offset, rowY, 0, taper, featherLength, greenColors - ); - } - } - - static renderSingleNeedle(ctx, x, y, baseAngle, taper, length, colors) { - const angleVar = Math.PI * 0.02; - const angle = baseAngle + (angleVar * seededRandom(x, y) - angleVar/2); - - ctx.strokeStyle = utils.getRandomColorFromPalette(colors, x, y); - ctx.beginPath(); - ctx.moveTo(x, y); - ctx.lineTo( - x + Math.cos(angle) * length * taper, - y + Math.sin(angle) * length * taper - ); - ctx.stroke(); - } - - static renderRowNeedles(ctx, x, rowY, rowWidth, featherCount, t, taper, featherLength, greenColors) { - for (let i = 0; i < featherCount; i++) { - const featherT = i / (featherCount - 1); - const startX = x - rowWidth/2 + rowWidth * featherT; - - if (Math.abs(startX - x) < 1) continue; - - const relativeX = (startX - x) / (rowWidth/2); - const baseAngle = relativeX < 0 ? Math.PI : 0; - const upwardTilt = Math.PI * FIR_CONFIG.UPWARD_TILT * t; - const angle = baseAngle + - (FIR_CONFIG.ANGLE_VARIATION * seededRandom(startX, rowY) - FIR_CONFIG.ANGLE_VARIATION/2) - - upwardTilt; - - const lengthMultiplier = FIR_CONFIG.LENGTH_MULTIPLIER + (1 - t) * 0.3; - const finalLength = featherLength * lengthMultiplier * taper * - (0.9 + seededRandom(startX * rowY, rowY) * 0.2); - - ctx.strokeStyle = utils.getRandomColorFromPalette(greenColors, startX, rowY); - ctx.beginPath(); - ctx.moveTo(startX, rowY); - ctx.lineTo( - startX + Math.cos(angle) * finalLength, - rowY + Math.sin(angle) * finalLength - ); - ctx.stroke(); - } - } - - static renderEdgeNeedles(ctx, x, baseWidth, baseY, tipY, featherLength, greenColors) { - for (let i = 0; i < FIR_CONFIG.EDGE_FEATHER_COUNT; i++) { - const t = i / (FIR_CONFIG.EDGE_FEATHER_COUNT - 1); - const taper = utils.calculateTaper(t); - - if (taper <= 0.1) continue; - - // Left edge - FirTreeRenderer.renderEdgeNeedle( - ctx, x, baseWidth, baseY, tipY, t, taper, - featherLength, Math.PI, greenColors, true - ); - - // Right edge - FirTreeRenderer.renderEdgeNeedle( - ctx, x, baseWidth, baseY, tipY, t, taper, - featherLength, 0, greenColors, false - ); - } - } - - static renderEdgeNeedle(ctx, x, baseWidth, baseY, tipY, t, taper, length, baseAngle, colors, isLeft) { - const sign = isLeft ? -1 : 1; - const needleX = x + sign * (baseWidth/2 * taper - (baseWidth/2 * taper) * t); - const needleY = baseY - (baseY - tipY) * t; - - const upwardTilt = Math.PI * 0.1 * t; - const angle = baseAngle + - (Math.PI * 0.05 * seededRandom(needleX, needleY) - Math.PI * 0.025) - - upwardTilt; - - ctx.strokeStyle = utils.getRandomColorFromPalette(colors, needleX, needleY); - ctx.beginPath(); - ctx.moveTo(needleX, needleY); - ctx.lineTo( - needleX + Math.cos(angle) * length * taper * 1.2, - needleY + Math.sin(angle) * length * taper * 1.2 - ); - ctx.stroke(); - } -} - -// Helper function to darken/lighten colors -const shadeColor = (color, percent) => { - const num = parseInt(color.replace('#', ''), 16); - const amt = Math.round(2.55 * percent); - const R = (num >> 16) + amt; - const G = (num >> 8 & 0x00FF) + amt; - const B = (num & 0x0000FF) + amt; - return '#' + (0x1000000 + - (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 + - (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 + - (B < 255 ? (B < 1 ? 0 : B) : 255) - ).toString(16).slice(1); -}; - -// Add new maple tree render function -const renderMapleTree = (ctx, tree, groundY) => { - const { x, config } = tree; - const { - width, height, trunkHeight, canopyRadius, - leafClusters, trunkColor, canopyColor - } = config; - - // Draw trunk base with narrower width (changed from width/4 to width/5) - const trunkWidth = width/5.5; - ctx.fillStyle = trunkColor; - ctx.fillRect( - x - trunkWidth/2, - groundY - trunkHeight, - trunkWidth, - trunkHeight - ); - - // Add trunk hatching with updated width - addHatchingPattern( - ctx, - x, - groundY, - trunkWidth, - trunkHeight, - trunkColor - ); - - // Function to create an irregular polygon - const createIrregularPolygon = (centerX, centerY, radius, sides, seed1, seed2) => { - ctx.beginPath(); - - // Create points array first to allow smoothing - const points = []; - for (let i = 0; i < sides; i++) { - const angle = (i / sides) * Math.PI * 2; - - // Even more subtle variation range (0.95-1.05) - const radiusVariation = 0.95 + seededRandom(seed1 * i, seed2 * i) * 0.10; - const pointRadius = radius * radiusVariation; - - points.push({ - x: centerX + Math.cos(angle) * pointRadius, - y: centerY + Math.sin(angle) * pointRadius - }); - } - - // Start the path - ctx.moveTo(points[0].x, points[0].y); - - // Draw smooth curves through all points - for (let i = 0; i < points.length; i++) { - const current = points[i]; - const next = points[(i + 1) % points.length]; - const nextNext = points[(i + 2) % points.length]; - - // Calculate control points for bezier curve with more smoothing - const cp1x = current.x + (next.x - points[(i - 1 + points.length) % points.length].x) / 4; - const cp1y = current.y + (next.y - points[(i - 1 + points.length) % points.length].y) / 4; - const cp2x = next.x - (nextNext.x - current.x) / 4; - const cp2y = next.y - (nextNext.y - current.y) / 4; - - // Draw bezier curve - ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, next.x, next.y); - } - - ctx.closePath(); - }; - - // Draw single canopy as irregular polygon - const sides = Math.floor(48 + seededRandom(x, groundY) * 16); // 48-64 sides - const mainY = groundY - trunkHeight - canopyRadius; - - ctx.fillStyle = canopyColor; - createIrregularPolygon( - x, - mainY, - canopyRadius * 1.4, // Increased size - sides, - x, - groundY - ); - ctx.fill(); -}; - -// Main tree render function that handles both types -const renderTree = (ctx, tree, groundY) => { - if (tree.config.type === 'fir') { - renderFirTree(ctx, tree, groundY); - } else if (tree.config.type === 'maple') { - renderMapleTree(ctx, tree, groundY); - } -}; - -const renderRock = (ctx, rock, groundY) => { - const { x, config } = rock; - const { width, height, color, highlights } = config; - - // Draw main rock shape (slightly irregular pentagon) - ctx.fillStyle = color; - ctx.beginPath(); - ctx.moveTo(x - width/2, groundY); - ctx.lineTo(x - width/2 + width/6, groundY - height); - ctx.lineTo(x + width/3, groundY - height); - ctx.lineTo(x + width/2, groundY - height/2); - ctx.lineTo(x + width/2, groundY); - ctx.closePath(); - ctx.fill(); - - // Add highlights - ctx.fillStyle = highlights; - ctx.beginPath(); - ctx.moveTo(x - width/4, groundY - height); - ctx.lineTo(x, groundY - height); - ctx.lineTo(x + width/6, groundY - height/2); - ctx.lineTo(x - width/6, groundY - height/2); - ctx.closePath(); - ctx.fill(); -}; - -// Add grass rendering function -const renderGrass = (ctx, grass, groundY, time) => { - const { x, config } = grass; - const { width, height, color, shadowColor, glowing } = config; - - // Initialize or get grass state - const stateKey = `grass_${x}`; - if (!window.gameState.world.grassStates[stateKey]) { - window.gameState.world.grassStates[stateKey] = GrassState.create(x, height); - } - const state = GrassState.validate(window.gameState.world.grassStates[stateKey]); - - // Update interaction - const player = window.gameState.player; - const distanceToPlayer = Math.abs(player.x + player.width / 2 - x); - GrassState.update(state, distanceToPlayer, width); - - // Handle rendering - setupGlowEffect(ctx, glowing, color, time); - renderGrassBlades(ctx, state, x, width, height, groundY, time, color, shadowColor); - resetGlowEffect(ctx, glowing); -}; - -const setupGlowEffect = (ctx, glowing, color, time) => { - if (glowing) { - ctx.save(); - ctx.shadowColor = color; - ctx.shadowBlur = 10 + Math.sin(time/GRASS_CONFIG.GLOW_SPEED) * 3; - } -}; - -const resetGlowEffect = (ctx, glowing) => { - if (glowing) { - ctx.restore(); - } -}; - -const renderGrassBlades = (ctx, state, x, width, height, groundY, time, color, shadowColor) => { - const bladeWidth = width / GRASS_CONFIG.BASE_BLADE_WIDTH * 0.7; - - for (let i = 0; i < state.bladeCount; i++) { - renderSingleBlade( - ctx, i, state, x, width, height, groundY, time, - bladeWidth, color, shadowColor - ); - } -}; - -const renderSingleBlade = ( - ctx, index, state, x, width, height, groundY, time, - bladeWidth, color, shadowColor -) => { - const centerOffset = (index - (state.bladeCount - 1) / 2) * - (width * GRASS_CONFIG.SPREAD_FACTOR / state.bladeCount); - const bladeX = x + centerOffset; - - const rustleOffset = Math.sin(time / GRASS_CONFIG.RUSTLE_SPEED + index) * 5 * state.rustleAmount; - const baseWave = Math.sin(time / GRASS_CONFIG.WAVE_SPEED + index) * 2; - - drawBladeCurve( - ctx, bladeX, groundY, height, rustleOffset, baseWave, - bladeWidth, index % 2 === 0 ? color : shadowColor - ); -}; - -const drawBladeCurve = ( - ctx, bladeX, groundY, height, rustleOffset, baseWave, - bladeWidth, color -) => { - const cp1x = bladeX + rustleOffset + baseWave; - const cp1y = groundY - height / 2; - const cp2x = bladeX + rustleOffset * 1.5 + baseWave; - const cp2y = groundY - height; - - ctx.fillStyle = color; - ctx.beginPath(); - ctx.moveTo(bladeX, groundY); - ctx.quadraticCurveTo(cp1x, cp1y, cp2x, cp2y); - ctx.quadraticCurveTo(cp1x + bladeWidth, cp1y, bladeX + bladeWidth, groundY); - ctx.fill(); -}; - -// Collision detection helper -const checkCollision = (player, platform) => { - return player.x < platform.x + platform.width && - player.x + player.width > platform.x && - player.y < platform.y + platform.height && - player.y + player.height > platform.y; -}; - -// World physics helper -const handleWorldCollisions = (player, world) => { - let onGround = false; - - // Check ground collision first - const groundY = window.innerHeight - world.groundHeight; - if (player.y + player.height > groundY) { - player.y = groundY - player.height; - player.velocityY = 0; - onGround = true; - } - - // Then check platform collisions - for (const platform of world.platforms) { - if (checkCollision(player, platform)) { - // Calculate overlap on each axis - const overlapX = Math.min( - player.x + player.width - platform.x, - platform.x + platform.width - player.x - ); - const overlapY = Math.min( - player.y + player.height - platform.y, - platform.y + platform.height - player.y - ); - - // Resolve collision on the axis with smallest overlap - if (overlapX < overlapY) { - // Horizontal collision - if (player.x < platform.x) { - player.x = platform.x - player.width; - } else { - player.x = platform.x + platform.width; - } - player.velocityX = 0; - } else { - // Vertical collision - if (player.y < platform.y) { - player.y = platform.y - player.height; - player.velocityY = 0; - onGround = true; - } else { - player.y = platform.y + platform.height; - player.velocityY = 0; - } - } - } - } - - return { ...player, jumping: !onGround }; -}; - -class GrassState { - static create(x, height) { - return { - rustleAmount: 0, - rustleDecay: 0.95, - bladeCount: utils.getBladeCount(x, height) - }; - } - - static validate(state) { - if (!state.bladeCount || state.bladeCount < GRASS_CONFIG.MIN_BLADES) { - state.bladeCount = GRASS_CONFIG.MIN_BLADES; - } - return state; - } - - static update(state, distanceToPlayer, interactionDistance) { - if (distanceToPlayer < interactionDistance) { - state.rustleAmount = Math.min(1, state.rustleAmount + 0.3); - } else { - state.rustleAmount *= state.rustleDecay; - } - } -} - -const renderFirTree = (ctx, tree, groundY) => { - const { x, config } = tree; - const { width, height, canopyOffset, trunkColor, canopyColor } = config; - - // Setup - const greenColors = NATURE_COLORS.GREENS.filter(color => color !== canopyColor); - ctx.lineWidth = FIR_CONFIG.FEATHER_WIDTH; - - // Render trunk - FirTreeRenderer.renderTrunk( - ctx, x, width, height - (height - canopyOffset)/2, - groundY, trunkColor - ); - - const drawFeatheredTree = (baseWidth, baseY, tipY) => { - const featherLength = baseWidth * 0.3; - - for (let row = 0; row < FIR_CONFIG.ROW_COUNT; row++) { - const t = row / (FIR_CONFIG.ROW_COUNT - 1); - const rowY = baseY - (baseY - tipY) * t; - - const taper = utils.calculateTaper(t); - const rowWidth = baseWidth * taper; - - if (rowWidth < 2) continue; - - const featherCount = Math.max(2, Math.floor(FIR_CONFIG.FEATHERS_PER_ROW * taper)); - - // Render center column of needles - FirTreeRenderer.renderCenterNeedles(ctx, x, rowY, taper, featherLength, greenColors); - - // Render row needles - FirTreeRenderer.renderRowNeedles( - ctx, x, rowY, rowWidth, featherCount, t, - taper, featherLength, greenColors - ); - } - - // Render edge needles - FirTreeRenderer.renderEdgeNeedles( - ctx, x, baseWidth, baseY, tipY, featherLength, greenColors - ); - }; - - // Draw complete tree - drawFeatheredTree( - width * 1.2, - groundY - canopyOffset, - groundY - height * 1.1 - ); -}; |