Initial commit

This commit is contained in:
Evert Prants 2019-07-31 21:32:08 +03:00
commit d2f693ccf5
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
11 changed files with 775 additions and 0 deletions

14
.babelrc Normal file
View File

@ -0,0 +1,14 @@
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": false,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}

14
.editorconfig Normal file
View File

@ -0,0 +1,14 @@
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.js]
charset = utf-8
indent_style = space
indent_size = 2

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/node_modules/
/dist/
/package-lock.json
/**/dna.txt
*.db
*.sqlite3

10
index.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<style type="text/css">*{margin:0;padding:0;}body{width:100%;height:100%;}body,html{overflow:hidden;}</style>
<title>bechunked</title>
</head>
<body>
</body>
</html>

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "bechunked",
"version": "0.0.0",
"description": "testing canvas stuff",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"serve": "node ./serve.js",
"build": "webpack -p",
"watch": "webpack -w --mode=development"
},
"keywords": [],
"private": true,
"author": "Evert \"Diamond\" Prants <evert@lunasqu.ee>",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.1.6",
"@babel/plugin-transform-runtime": "^7.1.0",
"@babel/preset-env": "^7.1.6",
"babel-loader": "^8.0.4",
"copy-webpack-plugin": "^5.0.4",
"express": "^4.16.4",
"html-webpack-plugin": "^3.2.0",
"standard": "^12.0.1",
"webpack": "^4.26.0",
"webpack-command": "^0.4.2"
},
"dependencies": {
"@babel/runtime": "^7.1.5",
"simplex-noise": "^2.4.0"
}
}

11
serve.js Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env node
const express = require('express')
const path = require('path')
const app = express()
app.set('env', 'development')
app.use('/', express.static(path.join(__dirname, 'dist')))
app.listen(3000, function () {
console.log('server listening on 0.0.0.0:3000')
})

63
src/image.js Normal file
View File

@ -0,0 +1,63 @@
import Resource from './resource'
let imageCache = {}
class Image {
constructor (file, img) {
this.file = file
this.img = img
}
static async load (file) {
if (imageCache[file]) return imageCache[file]
let img = await Resource.loadImage(file)
let imgCl = new Image(file, img)
imageCache[file] = imgCl
return imgCl
}
get width () {
return this.img.width
}
get height () {
return this.img.height
}
draw (ctx, x, y, w, h) {
ctx.drawImage(this.img, x, y, w, h)
}
}
class TileMap extends Image {
constructor (file, img, xaspect, yaspect) {
super(file, img)
this.xaspect = xaspect
this.yaspect = yaspect
this.xcount = this.width / xaspect
this.ycount = this.height / yaspect
this.tiles = []
for (let y = 0; y < this.ycount; y++) {
for (let x = 0; x < this.xcount; x++) {
this.tiles.push({ x, y })
}
}
}
draw (ctx, index, x, y, w, h) {
let box = this.tiles[index]
if (!box) return
ctx.drawImage(this.img, box.x * this.xaspect, box.y * this.yaspect, this.xaspect, this.yaspect, x, y, w, h)
}
static async load (file, x, y) {
if (imageCache[file]) return imageCache[file]
let img = await Resource.loadImage(file, true)
let imgCl = new TileMap(file, img, x, y)
imageCache[file] = imgCl
return imgCl
}
}
export { Image, TileMap }

271
src/index.js Normal file
View File

@ -0,0 +1,271 @@
/* 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()

222
src/input.js Normal file
View File

@ -0,0 +1,222 @@
const specialKeyMap = {
'backspace': 8,
'tab': 9,
'enter': 13,
'shift': 16,
'ctrl': 17,
'alt': 18,
'pausebreak': 19,
'capslock': 20,
'escape': 27,
'pgup': 33,
'pgdown': 34,
'end': 35,
'home': 36,
'left': 37,
'up': 38,
'right': 39,
'down': 40,
'insert': 45,
'delete': 46,
'left-window': 91,
'right-window': 92,
'select': 93,
'numpad0': 96,
'numpad1': 97,
'numpad2': 98,
'numpad3': 99,
'numpad4': 100,
'numpad5': 101,
'numpad6': 102,
'numpad7': 103,
'numpad8': 104,
'numpad9': 105,
'multiply': 106,
'add': 107,
'subtract': 109,
'decimal': 110,
'divide': 111,
'f1': 112,
'f2': 113,
'f3': 114,
'f4': 115,
'f5': 116,
'f6': 117,
'f7': 118,
'f8': 119,
'f9': 120,
'f10': 121,
'f11': 122,
'f12': 123,
'numlock': 144,
'scrolllock': 145,
'semi-colon': 186,
'equals': 187,
'comma': 188,
'dash': 189,
'period': 190,
'fwdslash': 191,
'grave': 192,
'open-bracket': 219,
'bkslash': 220,
'close-braket': 221,
'single-quote': 222
}
class Input {
constructor (canvas) {
this.keyList = {}
this.previousKeyList = {}
this.canvas = canvas
this.mouse = {
pos: { x: 0, y: 0 },
frame: { x: 0, y: 0 },
previous: { x: 0, y: 0 }
}
window.addEventListener('keydown', (e) => this.keyDown(e), false)
window.addEventListener('keyup', (e) => this.keyUp(e), false)
canvas.addEventListener('mousemove', (e) => {
let x
let y
if (e.changedTouches) {
let touch = e.changedTouches[0]
if (touch) {
e.pageX = touch.pageX
e.pageY = touch.pageY
}
}
if (e.pageX || e.pageY) {
x = e.pageX
y = e.pageY
} else {
x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft
y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop
}
x -= canvas.offsetLeft
y -= canvas.offsetTop
this.mouse.frame.x = x
this.mouse.frame.y = y
}, false)
canvas.addEventListener('mousedown', (e) => {
e.preventDefault()
this.mouse['btn' + e.button] = true
})
canvas.addEventListener('mouseup', (e) => {
e.preventDefault()
this.mouse['btn' + e.button] = false
})
canvas.addEventListener('contextmenu', (e) => {
e.preventDefault()
})
}
get mousePos () {
return this.mouse.pos
}
get mouseMoved () {
return (this.mouse.pos.x !== this.mouse.previous.x ||
this.mouse.pos.y !== this.mouse.previous.y)
}
get mouseOffset () {
return {
x: this.mouse.previous.x - this.mouse.pos.x,
y: this.mouse.previous.y - this.mouse.pos.y
}
}
toggleKey (keyCode, on) {
// Find key in special key list
let key = null
for (let k in specialKeyMap) {
let val = specialKeyMap[k]
if (keyCode === val) {
key = k
break
}
}
// Use fromCharCode
if (!key) {
key = String.fromCharCode(keyCode).toLowerCase()
}
this.keyList[key] = (on === true)
}
keyDown (e) {
let keycode
if (window.event) {
keycode = window.event.keyCode
} else if (e) {
keycode = e.which
}
this.toggleKey(keycode, true)
}
keyUp (e) {
let keycode
if (window.event) {
keycode = window.event.keyCode
} else if (e) {
keycode = e.which
}
this.toggleKey(keycode, false)
}
down (key) {
return this.keyList[key] != null ? this.keyList[key] : false
}
downLast (key) {
return this.previousKeyList[key] != null ? this.previousKeyList[key] : false
}
isDown (key) {
return this.down(key) && this.downLast(key)
}
isUp (key) {
return !this.isDown(key)
}
isPressed (key) {
return this.down(key) === true && this.downLast(key) === false
}
update () {
this.previousKeyList = {}
for (let k in this.keyList) {
if (this.keyList[k] === true) {
this.previousKeyList[k] = true
}
}
// Mouse positions in the previous frame
this.mouse.previous.x = this.mouse.pos.x
this.mouse.previous.y = this.mouse.pos.y
// Mouse positions in the current frame
// Convert to OpenGL coordinate system
this.mouse.pos.x = this.mouse.frame.x / this.canvas.width * 2 - 1
this.mouse.pos.y = this.mouse.frame.y / this.canvas.height * 2 - 1
}
}
export default Input

101
src/resource.js Normal file
View File

@ -0,0 +1,101 @@
/* global XMLHttpRequest, Image */
let imgCache = {}
function powerOfTwo (n) {
return n && (n & (n - 1)) === 0
}
function GET (url, istext) {
return new Promise((resolve, reject) => {
var xmlHttp = new XMLHttpRequest()
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
resolve(xmlHttp.responseText)
} else if (xmlHttp.readyState === 4 && xmlHttp.status >= 400) {
let err = new Error(xmlHttp.status)
err.request = xmlHttp
reject(err)
}
}
xmlHttp.open('GET', url, true)
istext && (xmlHttp.responseType = 'text')
xmlHttp.send(null)
})
}
function smartGET (data) {
if (typeof data === 'string') {
data = {
url: data,
type: 'text'
}
}
if (!data.type) data.type = 'text'
let istext = (data.type !== 'image' && data.type !== 'file')
let url = data.url
if (!url) throw new Error('URL is required!')
if (data.type === 'json') {
return new Promise((resolve, reject) => {
GET(url).then((dtext) => {
try {
let jsonp = JSON.parse(dtext)
return resolve(jsonp)
} catch (e) {
reject(e)
}
}, reject)
})
}
return GET(data.url, istext)
}
function loadImage (url, nowarn = false) {
url = '/assets/images/' + url
// Ensure we don't load a texture multiple times
if (imgCache[url]) return new Promise((resolve, reject) => resolve(imgCache[url]))
return new Promise((resolve, reject) => {
let img = new Image()
img.onload = function () {
// Friendly warnings
if ((!powerOfTwo(img.width) || !powerOfTwo(img.height)) && !nowarn) {
console.warn(`warn: image ${url} does not have dimensions that are powers of two. 16x16 to 128x128 recommended.`)
}
if ((img.width / img.height) !== 1 && !nowarn) {
console.warn(`warn: image ${url} does not have an aspect ratio of 1 to 1.`)
}
imgCache[url] = img
resolve(img)
}
img.onerror = function (e) {
reject(e)
}
img.src = url
})
}
function imageToSampler (img) {
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0, img.width, img.height)
return function (x, y) {
return ctx.getImageData(x, y, 1, 1).data
}
}
export default { GET: smartGET, imageToSampler, loadImage }

32
webpack.config.js Normal file
View File

@ -0,0 +1,32 @@
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = (env) => {
return {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'app.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
devtool: env.mode === 'development' ? 'inline-source-map' : '',
plugins: [
new HtmlWebpackPlugin({ template: 'index.html' }),
new CopyWebpackPlugin([ { from: 'static' } ])
]
}
}