diff options
Diffstat (limited to 'html/side-scrolling-rogue-thing/js')
-rw-r--r-- | html/side-scrolling-rogue-thing/js/camera.js | 56 | ||||
-rw-r--r-- | html/side-scrolling-rogue-thing/js/config.js | 32 | ||||
-rw-r--r-- | html/side-scrolling-rogue-thing/js/input.js | 19 | ||||
-rw-r--r-- | html/side-scrolling-rogue-thing/js/player.js | 64 | ||||
-rw-r--r-- | html/side-scrolling-rogue-thing/js/renderer.js | 97 | ||||
-rw-r--r-- | html/side-scrolling-rogue-thing/js/rogue.js | 144 | ||||
-rw-r--r-- | html/side-scrolling-rogue-thing/js/world.js | 1054 |
7 files changed, 1466 insertions, 0 deletions
diff --git a/html/side-scrolling-rogue-thing/js/camera.js b/html/side-scrolling-rogue-thing/js/camera.js new file mode 100644 index 0000000..e5d5d14 --- /dev/null +++ b/html/side-scrolling-rogue-thing/js/camera.js @@ -0,0 +1,56 @@ +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/side-scrolling-rogue-thing/js/config.js b/html/side-scrolling-rogue-thing/js/config.js new file mode 100644 index 0000000..77100e1 --- /dev/null +++ b/html/side-scrolling-rogue-thing/js/config.js @@ -0,0 +1,32 @@ +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/side-scrolling-rogue-thing/js/input.js b/html/side-scrolling-rogue-thing/js/input.js new file mode 100644 index 0000000..047321c --- /dev/null +++ b/html/side-scrolling-rogue-thing/js/input.js @@ -0,0 +1,19 @@ +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/side-scrolling-rogue-thing/js/player.js b/html/side-scrolling-rogue-thing/js/player.js new file mode 100644 index 0000000..270b26f --- /dev/null +++ b/html/side-scrolling-rogue-thing/js/player.js @@ -0,0 +1,64 @@ +const createPlayer = (x, y) => ({ + x, + y, + width: 32, + height: 32, + velocityX: 0, + velocityY: 0, + speed: 300, + jumping: false +}); + +const updatePlayer = (player, deltaTime) => { + const keys = getKeys(); + const seconds = deltaTime / 1000; + + let velocityX = 0; + let velocityY = player.velocityY; + + // Horizontal movement + if (keys.ArrowLeft) velocityX -= player.speed; + if (keys.ArrowRight) velocityX += player.speed; + + // 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 + }; + + // Handle collisions with the world + return handleWorldCollisions(updatedPlayer, window.gameState.world); +}; + +const renderPlayer = (ctx, player) => { + ctx.fillStyle = '#f00'; + ctx.fillRect(player.x, player.y, player.width, player.height); +}; + +// Key handling +const keys = {}; + +window.addEventListener('keydown', (e) => { + keys[e.key] = true; +}); + +window.addEventListener('keyup', (e) => { + keys[e.key] = false; +}); + +const getKeys = () => ({...keys}); diff --git a/html/side-scrolling-rogue-thing/js/renderer.js b/html/side-scrolling-rogue-thing/js/renderer.js new file mode 100644 index 0000000..617b4c7 --- /dev/null +++ b/html/side-scrolling-rogue-thing/js/renderer.js @@ -0,0 +1,97 @@ +// 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/side-scrolling-rogue-thing/js/rogue.js b/html/side-scrolling-rogue-thing/js/rogue.js new file mode 100644 index 0000000..64716a4 --- /dev/null +++ b/html/side-scrolling-rogue-thing/js/rogue.js @@ -0,0 +1,144 @@ +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/side-scrolling-rogue-thing/js/world.js b/html/side-scrolling-rogue-thing/js/world.js new file mode 100644 index 0000000..2b2529a --- /dev/null +++ b/html/side-scrolling-rogue-thing/js/world.js @@ -0,0 +1,1054 @@ +// 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 + ); +}; |