272 lines
6.8 KiB
JavaScript
272 lines
6.8 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 = []
|
|
}
|
|
|
|
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) {
|
|
let positions = ChunkWorld.getPositionsInViewport(pos, scale)
|
|
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)
|
|
ChunkWorld.generateTerrain(chunk)
|
|
this.chunks.push(chunk)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
render (r, o, s) {
|
|
for (let i in this.chunks) {
|
|
let chunk = this.chunks[i]
|
|
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 aw = Math.ceil(canvas.width / scaledSize) + 1
|
|
let ah = Math.ceil(canvas.height / scaledSize)
|
|
|
|
if (chunk.x <= localX + aw && chunk.x + 1 >= localX &&
|
|
chunk.y <= localY + ah && 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
|
|
} else if (input.isDown('s')) {
|
|
cursor.y -= 6
|
|
}
|
|
|
|
if (input.isDown('a')) {
|
|
cursor.x += 6
|
|
} else if (input.isDown('d')) {
|
|
cursor.x -= 6
|
|
}
|
|
|
|
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()
|