/********************************************************************** * TERRAIN GENERATION **********************************************************************/ // Perlin noise implementation // Based on https://github.com/josephg/noisejs class Perlin { constructor() { this.grad3 = [[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0], [1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1], [0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]]; this.p = []; for (let i=0; i<256; i++) { this.p[i] = Math.floor(Math.random()*256); } // To remove the need for index wrapping, double the permutation table length this.perm = new Array(512); this.gradP = new Array(512); // Skipping the seed function for simplicity this.seed(0); } seed(seed) { if(seed > 0 && seed < 1) { // Scale the seed out seed *= 65536; } seed = Math.floor(seed); if(seed < 256) { seed |= seed << 8; } for(let i = 0; i < 256; i++) { let v; if (i & 1) { v = this.p[i] ^ (seed & 255); } else { v = this.p[i] ^ ((seed>>8) & 255); } this.perm[i] = this.perm[i + 256] = v; this.gradP[i] = this.gradP[i + 256] = this.grad3[v % 12]; } } // 2D Perlin Noise noise(x, y) { // Find unit grid cell containing point let X = Math.floor(x), Y = Math.floor(y); // Get relative xy coordinates of point within that cell x = x - X; y = y - Y; // Wrap the integer cells at 255 (smaller integer period can be introduced here) X = X & 255; Y = Y & 255; // Calculate noise contributions from each of the four corners let n00 = this.dot2(this.gradP[X+this.perm[Y]], x, y); let n01 = this.dot2(this.gradP[X+this.perm[Y+1]], x, y-1); let n10 = this.dot2(this.gradP[X+1+this.perm[Y]], x-1, y); let n11 = this.dot2(this.gradP[X+1+this.perm[Y+1]], x-1, y-1); // Compute the fade curve value for x let u = this.fade(x); // Interpolate the four results return this.lerp( this.lerp(n00, n10, u), this.lerp(n01, n11, u), this.fade(y)); } // Fading function fade(t) { return t*t*t*(t*(t*6-15)+10); } // Linear interpolation lerp(a, b, t) { return (1-t)*a + t*b; } // Dot product dot2(g, x, y) { return g[0]*x + g[1]*y; } } // Terrain types const TERRAIN_WATER = 0; const TERRAIN_GRASS = 1; const TERRAIN_SAND = 2; const TERRAIN_DIRT = 3; const TERRAIN_STONE = 4; // Generate terrain map function generateTerrain(width, height, scale) { const terrain = new Array(width); for (let x = 0; x < width; x++) { terrain[x] = new Array(height); for (let y = 0; y < height; y++) { // Generate noise value let nx = x / scale; let ny = y / scale; let value = perlin.noise(nx, ny); // Adjust the value to get more interesting terrain // Add multiple octaves of noise for more natural look value += 0.5 * perlin.noise(nx * 2, ny * 2); value += 0.25 * perlin.noise(nx * 4, ny * 4); value /= 1.75; // Normalize // Generate a second noise value for stone distribution let stoneNoise = perlin.noise(nx * 3, ny * 3); // Determine terrain type based on noise value // Adjusted to get more water if (value < -0.2) { // More water terrain[x][y] = TERRAIN_WATER; // Check for sand near water (beach) let sandCheck = perlin.noise((nx + 0.1) * 8, (ny + 0.1) * 8); if (value > -0.25 && sandCheck > 0) { terrain[x][y] = TERRAIN_SAND; } } else if (value < 0.0) { // Sand appears near water terrain[x][y] = TERRAIN_SAND; } else if (value < 0.3) { // Grass in middle elevations terrain[x][y] = TERRAIN_GRASS; } else if (stoneNoise > 0.3) { // Stone in higher elevations with specific noise pattern terrain[x][y] = TERRAIN_STONE; } else { // Dirt in higher elevations terrain[x][y] = TERRAIN_DIRT; } } } // Second pass to smooth terrain and create better beaches smoothTerrain(terrain, width, height); return terrain; } function smoothTerrain(terrain, width, height) { // Create sand around water for (let x = 1; x < width - 1; x++) { for (let y = 1; y < height - 1; y++) { if (terrain[x][y] !== TERRAIN_WATER) { // Check if adjacent to water let adjacentToWater = false; for (let dx = -1; dx <= 1; dx++) { for (let dy = -1; dy <= 1; dy++) { if (x + dx >= 0 && x + dx < width && y + dy >= 0 && y + dy < height) { if (terrain[x + dx][y + dy] === TERRAIN_WATER) { adjacentToWater = true; break; } } } if (adjacentToWater) break; } // If adjacent to water and not already sand, make it sand if (adjacentToWater && terrain[x][y] !== TERRAIN_SAND && Math.random() > 0.3) { terrain[x][y] = TERRAIN_SAND; } } } } } // Check if a position is in water function isWater(x, y) { return getTerrainType(x, y) === TERRAIN_WATER; } // Get terrain type at a position function getTerrainType(x, y) { // Convert world coordinates to terrain grid coordinates const gridX = Math.floor((x + 2000) / 10); const gridY = Math.floor((y + 2000) / 10); // Check bounds if (gridX < 0 || gridX >= terrainWidth || gridY < 0 || gridY >= terrainHeight) { return TERRAIN_GRASS; // Default to grass if out of bounds } return terrainMap[gridX][gridY]; } // Set terrain type at a position function setTerrainType(x, y, type) { // Convert world coordinates to terrain grid coordinates const gridX = Math.floor((x + 2000) / 10); const gridY = Math.floor((y + 2000) / 10); // Check bounds if (gridX < 0 || gridX >= terrainWidth || gridY < 0 || gridY >= terrainHeight) { return false; // Can't set if out of bounds } terrainMap[gridX][gridY] = type; return true; } // Get terrain name from type function getTerrainName(type) { switch(type) { case TERRAIN_WATER: return "Water"; case TERRAIN_GRASS: return "Grass"; case TERRAIN_SAND: return "Sand"; case TERRAIN_DIRT: return "Dirt"; case TERRAIN_STONE: return "Stone"; default: return "Unknown"; } } // Terrain dimensions const terrainWidth = 400; const terrainHeight = 400; const terrainScale = 50; // Smaller scale = more detailed terrain // Initialize Perlin noise const perlin = new Perlin(); // Generate the terrain map let terrainMap = generateTerrain(terrainWidth, terrainHeight, terrainScale);