/* global requestAnimationFrame */ import Simplex from 'simplex-noise' import Input from './input' class BetterNoise extends Simplex { // amplitude - Controls the amount the height changes. The higher, the taller the hills. // persistence - Controls details, value in [0,1]. Higher increases grain, lower increases smoothness. // octaves - Number of noise layers // period - Distance above which we start to see similarities. The higher, the longer "hills" will be on a terrain. // lacunarity - Controls period change across octaves. 2 is usually a good value to address all detail levels. constructor (seed, amplitude = 15, persistence = 0.4, octaves = 5, period = 80, lacunarity = 2) { super(seed) this.seed = seed this.amplitude = amplitude this.period = period this.lacunarity = lacunarity this.octaves = octaves this.persistence = persistence } getNoise (zx, zy) { let x = zx / this.period let y = zy / this.period let amp = 1.0 let max = 1.0 let sum = this.noise2D(x, y) let i = 0 while (++i < this.octaves) { x *= this.lacunarity y *= this.lacunarity amp *= this.persistence max += amp sum += this.noise2D(x, y) * amp } return (sum / max) * this.amplitude } } const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') document.body.appendChild(canvas) canvas.width = window.innerWidth canvas.height = window.innerHeight const ChunkSize = 16 const TileSize = 32 const TotalSize = ChunkSize * TileSize class Tile { constructor (name, color = '#000', image, size = TileSize) { this.name = name this.size = size this.color = color this.image = image } // Render tile at precise render coordinates on the canvas, no relativity draw (r, x, y, s = 1) { let scaledSize = this.size / s if (this.image) { if (this.image.tiles && this.index != null) { return this.image.draw(r, this.index, x, y, scaledSize, scaledSize) } if (this.image.src) { return r.drawImage(this.image, x, y, scaledSize, scaledSize) } return this.image.draw(r, x, y, scaledSize, scaledSize) } r.fillStyle = this.color r.fillRect(x, y, scaledSize, scaledSize) } } class Chunk { constructor (x, y) { this.x = x this.y = y this.tiles = {} } getTile (x, y) { if (x > ChunkSize || y > ChunkSize || x < 0 || y < 0) throw new Error('Out Of Bounds!') return this.getTilei(Chunk.ptoi(x, y)) } getTilei (i) { if (i < 0 || i > ChunkSize * ChunkSize) return null return this.tiles[i] } setTile (x, y, j) { if (x > ChunkSize || y > ChunkSize || x < 0 || y < 0) throw new Error('Out Of Bounds!') this.setTilei(Chunk.ptoi(x, y), j) } setTilei (i, j) { if (i < 0 || i > ChunkSize * ChunkSize) throw new Error('Out Of Bounds!') this.tiles[i] = j } // Index -> 2D Position static itop (i) { return { x: Math.floor(i % ChunkSize), y: Math.floor(i / ChunkSize) } } // 2D Position -> Index static ptoi (x, y) { return x + y * ChunkSize } // @r : canvas context // @c : chunk // @o : offset // @s : scale static render (r, c, o = { x: 0, y: 0 }, s = 1) { if (!c) return null for (let i in c.tiles) { let tile = c.tiles[i] if (tile == null || !(tile instanceof Tile)) continue let pos = Chunk.itop(i) tile.draw(r, Math.floor(((pos.x * TileSize / s) + (c.x * TotalSize / s)) + o.x), Math.floor(((pos.y * TileSize / s) + (c.y * TotalSize / s)) + o.y), s ) } } } class ChunkWorld { constructor () { this.chunks = [] this.dirty = true } mkdirty () { this.dirty = true } static generateTerrain (c) { for (let i = 0; i < 16; i++) { for (let j = 0; j < 16; j++) { let h = noise.getNoise(i + (c.x * ChunkSize), j + (c.y * ChunkSize)) let tile = 0 if (h > noise.amplitude * 0.5) { tile = 3 } else if (h > noise.amplitude * 0.3) { tile = 2 } else if (h > noise.amplitude * 0.2) { tile = 1 } c.setTile(i, j, tiles[tile]) } } return c } static getPositionsInViewport (pos, scale) { // Calculate how many chunks could theoretically fit in this viewport let swidth = Math.ceil(canvas.width / (TotalSize / scale)) + 1 let sheight = Math.ceil(canvas.height / (TotalSize / scale)) + 1 let positions = [] // Loop through the theoretical positions for (let px = 0; px < swidth; px++) { for (let py = 0; py < sheight; py++) { let cx = (px * (TotalSize / scale)) - pos.x let cy = (py * (TotalSize / scale)) - pos.y positions.push({ x: Math.floor(cx / (TotalSize / scale)), y: Math.floor(cy / (TotalSize / scale)) }) } } return positions } update (pos, scale) { if (!this.dirty) return let positions = ChunkWorld.getPositionsInViewport(pos, scale) let generated = false for (let p in positions) { let cpos = positions[p] let found = false for (let i in this.chunks) { let chunk = this.chunks[i] if (chunk.x === cpos.x && chunk.y === cpos.y) { found = true break } } if (!found) { let chunk = new Chunk(cpos.x, cpos.y) generated = true ChunkWorld.generateTerrain(chunk) this.chunks.push(chunk) break } } // We generated nothing this time around, mark it as not dirty if (!generated) { this.dirty = false } } render (r, o, s) { let scaledSize = TotalSize / s // Convert to chunk-space coordinates let localX = Math.floor(cursor.x / scaledSize) * -1 let localY = Math.floor(cursor.y / scaledSize) * -1 let localW = Math.ceil(canvas.width / scaledSize) + 1 let localH = Math.ceil(canvas.height / scaledSize) for (let i in this.chunks) { let chunk = this.chunks[i] if (chunk.x <= localX + localW && chunk.x + 1 >= localX && chunk.y <= localY + localH && chunk.y + 1 >= localY) { Chunk.render(r, chunk, o, s) } } } } let world = new ChunkWorld() let input = new Input(canvas) let noise = new BetterNoise(1, /* amplitude = */15, /* persistence = */0.4, /* octaves = */5, /* period = */120, /* lacunarity = */2) let zoom = 2 let tiles = [ new Tile('water', '#028fdb'), new Tile('sand', '#cec673'), new Tile('grass', '#007c1f'), new Tile('stone', '#515151') ] let cursor = { x: 0, y: 0 } function render () { ctx.fillStyle = '#00aaff' ctx.fillRect(0, 0, canvas.width, canvas.height) world.render(ctx, cursor, zoom) } function update (dt) { if (input.isDown('w')) { cursor.y += 6 world.mkdirty() } else if (input.isDown('s')) { cursor.y -= 6 world.mkdirty() } if (input.isDown('a')) { cursor.x += 6 world.mkdirty() } else if (input.isDown('d')) { cursor.x -= 6 world.mkdirty() } world.update(cursor, zoom) input.update() } function gameLoop () { requestAnimationFrame(gameLoop) update() render() } window.addEventListener('resize', function () { canvas.width = window.innerWidth canvas.height = window.innerHeight }) gameLoop()