bechunked/src/index.js

291 lines
7.2 KiB
JavaScript

/* 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()