diff options
Diffstat (limited to 'html/rogue/js/world.js')
-rw-r--r-- | html/rogue/js/world.js | 535 |
1 files changed, 304 insertions, 231 deletions
diff --git a/html/rogue/js/world.js b/html/rogue/js/world.js index 789b5f4..2b2529a 100644 --- a/html/rogue/js/world.js +++ b/html/rogue/js/world.js @@ -1,3 +1,46 @@ +// 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', @@ -35,7 +78,7 @@ const MAPLE_TYPES = { SMALL: { type: 'maple', width: 100, - height: 130, + height: 330, trunkHeight: 60, canopyRadius: 35, leafClusters: 5, @@ -45,7 +88,7 @@ const MAPLE_TYPES = { MEDIUM: { type: 'maple', width: 130, - height: 160, + height: 360, trunkHeight: 70, canopyRadius: 45, leafClusters: 6, @@ -55,7 +98,7 @@ const MAPLE_TYPES = { LARGE: { type: 'maple', width: 160, - height: 200, + height: 400, trunkHeight: 85, canopyRadius: 55, leafClusters: 7, @@ -65,7 +108,7 @@ const MAPLE_TYPES = { BRIGHT: { type: 'maple', width: 140, - height: 180, + height: 380, trunkHeight: 75, canopyRadius: 50, leafClusters: 6, @@ -75,7 +118,7 @@ const MAPLE_TYPES = { SAGE: { type: 'maple', width: 150, - height: 190, + height: 490, trunkHeight: 80, canopyRadius: 52, leafClusters: 6, @@ -154,6 +197,22 @@ const GRASS_TYPES = { } }; +// 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 = { @@ -525,185 +584,122 @@ const addHatchingPattern = (ctx, x, y, width, height, color) => { ctx.restore(); }; -// Update renderFirTree function -const renderFirTree = (ctx, tree, groundY) => { - const { x, config } = tree; - const { width, height, canopyOffset, trunkColor, canopyColor } = config; - - // Calculate trunk dimensions - make trunk narrower - const trunkWidth = width/4; // Changed from /3 to /4 - const trunkHeight = height - (height - canopyOffset)/2; - - // Draw trunk base - ctx.fillStyle = trunkColor; - ctx.fillRect( - x - trunkWidth/2, - groundY - trunkHeight, - trunkWidth, - trunkHeight - ); - - // Add trunk hatching - addHatchingPattern( - ctx, - x, - groundY, - trunkWidth, - trunkHeight, - trunkColor - ); - - // Define a range of green colors for texture - const greenColors = [ - '#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 - ].filter(color => color !== canopyColor); - - const drawFeatheredTree = (baseWidth, baseY, tipY) => { - // Enhanced feathering parameters - const rowCount = 40; - const feathersPerRow = 24; - const featherLength = baseWidth * 0.3; - const featherWidth = 2; +class FirTreeRenderer { + static renderTrunk(ctx, x, width, height, groundY, trunkColor) { + const trunkWidth = width/4; + const trunkHeight = height; - ctx.lineWidth = featherWidth; + ctx.fillStyle = trunkColor; + ctx.fillRect( + x - trunkWidth/2, + groundY - trunkHeight, + trunkWidth, + trunkHeight + ); - // Draw rows of feathers from bottom to top - for (let row = 0; row < rowCount; row++) { - const t = row / (rowCount - 1); - const rowY = baseY - (baseY - tipY) * t; + 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); - // More aggressive width reduction near top - const taper = Math.pow(1 - t, 0.8); - const rowWidth = baseWidth * taper; + // Left needle + FirTreeRenderer.renderSingleNeedle( + ctx, x + offset, rowY, Math.PI, taper, featherLength, greenColors + ); - // Reduce feather count more aggressively near top - const featherCount = Math.max(2, Math.floor(feathersPerRow * taper)); + // 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; - // Skip drawing if width is too small (creates sharp point) - if (rowWidth < 2) continue; + if (Math.abs(startX - x) < 1) continue; - // Draw center column of needles first - const centerX = x; - const needleCount = 5; // Number of additional needles on each side - const needleSpacing = 1.5; // Spacing between additional needles - - for (let i = -needleCount; i <= needleCount; i++) { - // Calculate offset for each needle - const offset = i * (needleSpacing * 1.5); - - // Draw a needle pointing left - ctx.strokeStyle = greenColors[Math.floor(seededRandom(centerX + offset - 1, rowY) * greenColors.length)]; - ctx.beginPath(); - ctx.moveTo(centerX + offset - 1, rowY); // Start slightly left of center - const leftAngle = Math.PI + (Math.PI * 0.02 * seededRandom(centerX + offset, rowY) - Math.PI * 0.01); - ctx.lineTo( - centerX + offset + Math.cos(leftAngle) * featherLength * taper, - rowY + Math.sin(leftAngle) * featherLength * taper - ); - ctx.stroke(); - - // Draw a needle pointing right - ctx.strokeStyle = greenColors[Math.floor(seededRandom(centerX + offset + 1, rowY) * greenColors.length)]; - ctx.beginPath(); - ctx.moveTo(centerX + offset + 1, rowY); // Start slightly right of center - const rightAngle = 0 + (Math.PI * 0.02 * seededRandom(centerX + offset, rowY) - Math.PI * 0.01); - ctx.lineTo( - centerX + offset + Math.cos(rightAngle) * featherLength * taper, - rowY + Math.sin(rightAngle) * featherLength * taper - ); - ctx.stroke(); - } + 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; - // Draw regular feathers for this row - for (let i = 0; i < featherCount; i++) { - const featherT = i / (featherCount - 1); - const startX = x - rowWidth/2 + rowWidth * featherT; - - // Skip the center point as we've already drawn it - if (Math.abs(startX - x) < 1) continue; - - // Calculate angle to point directly away from center with slight upward tilt - const relativeX = (startX - x) / (rowWidth/2); - const baseAngle = relativeX < 0 ? Math.PI : 0; - const upwardTilt = Math.PI * 0.1 * t; - const angleVariation = Math.PI * 0.05; - const angle = baseAngle + (angleVariation * seededRandom(startX, rowY) - angleVariation/2) - upwardTilt; - - // Reduce length near top but keep needles longer - const lengthMultiplier = 0.8 + (1 - t) * 0.3; - const finalLength = featherLength * lengthMultiplier * taper * - (0.9 + seededRandom(startX * rowY, rowY) * 0.2); - - const colorIndex = Math.floor(seededRandom(startX, rowY) * greenColors.length); - ctx.strokeStyle = greenColors[colorIndex]; - - ctx.beginPath(); - ctx.moveTo(startX, rowY); - ctx.lineTo( - startX + Math.cos(angle) * finalLength, - rowY + Math.sin(angle) * finalLength - ); - ctx.stroke(); - } + 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(); } - - // Add extra feathers at the edges with upward tilt - const edgeFeatherCount = 40; - for (let i = 0; i < edgeFeatherCount; i++) { - const t = i / (edgeFeatherCount - 1); - const taper = Math.pow(1 - t, 0.8); + } + + 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); - // Left edge - const leftX = x - baseWidth/2 * taper + (baseWidth/2 * taper) * t; - const leftY = baseY - (baseY - tipY) * t; - const leftUpwardTilt = Math.PI * 0.1 * t; - const leftAngle = Math.PI + (Math.PI * 0.05 * seededRandom(leftX, leftY) - Math.PI * 0.025) - leftUpwardTilt; + if (taper <= 0.1) continue; - if (taper > 0.1) { - ctx.strokeStyle = greenColors[Math.floor(seededRandom(leftX, leftY) * greenColors.length)]; - ctx.beginPath(); - ctx.moveTo(leftX, leftY); - ctx.lineTo( - leftX + Math.cos(leftAngle) * featherLength * taper * 1.2, - leftY + Math.sin(leftAngle) * featherLength * taper * 1.2 - ); - ctx.stroke(); - } + // Left edge + FirTreeRenderer.renderEdgeNeedle( + ctx, x, baseWidth, baseY, tipY, t, taper, + featherLength, Math.PI, greenColors, true + ); // Right edge - const rightX = x + baseWidth/2 * taper - (baseWidth/2 * taper) * t; - const rightY = baseY - (baseY - tipY) * t; - const rightUpwardTilt = Math.PI * 0.1 * t; - const rightAngle = 0 + (Math.PI * 0.05 * seededRandom(rightX, rightY) - Math.PI * 0.025) - rightUpwardTilt; - - if (taper > 0.1) { - ctx.strokeStyle = greenColors[Math.floor(seededRandom(rightX, rightY) * greenColors.length)]; - ctx.beginPath(); - ctx.moveTo(rightX, rightY); - ctx.lineTo( - rightX + Math.cos(rightAngle) * featherLength * taper * 1.2, - rightY + Math.sin(rightAngle) * featherLength * taper * 1.2 - ); - ctx.stroke(); - } + FirTreeRenderer.renderEdgeNeedle( + ctx, x, baseWidth, baseY, tipY, t, taper, + featherLength, 0, greenColors, false + ); } - }; + } - // Draw feathered tree shape - drawFeatheredTree( - width * 1.2, - groundY - canopyOffset, - groundY - height * 1.1 - ); -}; + 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) => { @@ -844,83 +840,83 @@ const renderGrass = (ctx, grass, groundY, time) => { const { x, config } = grass; const { width, height, color, shadowColor, glowing } = config; - // Get or initialize grass state + // Initialize or get grass state const stateKey = `grass_${x}`; if (!window.gameState.world.grassStates[stateKey]) { - // Ensure blade count is between 4 and 8 (inclusive) - const minBlades = 4; - const maxBlades = 8; - // Use Math.round instead of Math.floor and ensure positive value - const randomValue = Math.abs(seededRandom(x, height)); - const bladeCount = minBlades + Math.round(randomValue * (maxBlades - minBlades)); - - window.gameState.world.grassStates[stateKey] = { - rustleAmount: 0, - rustleDecay: 0.95, - bladeCount: bladeCount - }; + window.gameState.world.grassStates[stateKey] = GrassState.create(x, height); } - const state = window.gameState.world.grassStates[stateKey]; - - // Additional validation as a safety net - if (!state.bladeCount || state.bladeCount < 4) { - state.bladeCount = 4; - } - - // Check for player interaction + const state = GrassState.validate(window.gameState.world.grassStates[stateKey]); + + // Update interaction const player = window.gameState.player; - const interactionDistance = width; const distanceToPlayer = Math.abs(player.x + player.width / 2 - x); - - if (distanceToPlayer < interactionDistance) { - state.rustleAmount = Math.min(1, state.rustleAmount + 0.3); - } else { - state.rustleAmount *= state.rustleDecay; - } + 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); +}; - // Add glow effect for blue grass +const setupGlowEffect = (ctx, glowing, color, time) => { if (glowing) { ctx.save(); ctx.shadowColor = color; - ctx.shadowBlur = 10 + Math.sin(time/500) * 3; - } - - // Draw each blade of grass - for (let i = 0; i < state.bladeCount; i++) { - // Calculate blade width - consistent regardless of blade count - const bladeWidth = width / 5 * 0.7; - - // Calculate blade position - cluster them together more tightly - const spreadFactor = 0.7; // Reduces the spread to create tighter clumps - const centerOffset = (i - (state.bladeCount - 1) / 2) * (width * spreadFactor / state.bladeCount); - const bladeX = x + centerOffset; - - // Calculate blade curve based on rustle and time - const rustleOffset = Math.sin(time / 300 + i) * 5 * state.rustleAmount; - const baseWave = Math.sin(time / 1000 + i) * 2; - - // Draw blade - ctx.fillStyle = i % 2 === 0 ? color : shadowColor; - ctx.beginPath(); - ctx.moveTo(bladeX, groundY); - - // Control points for quadratic curve - const cp1x = bladeX + rustleOffset + baseWave; - const cp1y = groundY - height / 2; - const cp2x = bladeX + rustleOffset * 1.5 + baseWave; - const cp2y = groundY - height; - - // Draw curved blade - ctx.quadraticCurveTo(cp1x, cp1y, cp2x, cp2y); - ctx.quadraticCurveTo(cp1x + bladeWidth, cp1y, bladeX + bladeWidth, groundY); - ctx.fill(); + 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 && @@ -979,3 +975,80 @@ const handleWorldCollisions = (player, world) => { 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 + ); +}; |