beginnings of another project that will never be finished, yay

This commit is contained in:
Evert Prants 2020-01-08 22:07:58 +02:00
commit 143a93f9f4
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
18 changed files with 1206 additions and 0 deletions

15
.babelrc Normal file
View File

@ -0,0 +1,15 @@
{
"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

BIN
assets/ground.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

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;}canvas{image-rendering: optimizeSpeed;image-rendering: -moz-crisp-edges;image-rendering: -webkit-optimize-contrast;image-rendering: -o-crisp-edges;image-rendering: pixelated;-ms-interpolation-mode: nearest-neighbor;}</style>
<title>tilegame</title>
</head>
<body>
</body>
</html>

28
package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "tilegame",
"version": "0.0.0",
"description": "Tile game",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"serve": "node .",
"build": "webpack -p",
"watch": "webpack -w --mode=development"
},
"keywords": [],
"private": true,
"author": "Evert \"Diamond\" Prants <evert@lunasqu.ee>",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.7.7",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.7",
"@babel/runtime": "^7.7.7",
"babel-loader": "^8.0.4",
"html-webpack-plugin": "^3.2.0",
"seedrandom": "^3.0.5",
"standard": "^12.0.1",
"webpack": "^4.41.5",
"webpack-command": "^0.4.2"
}
}

41
src/canvas.js Normal file
View File

@ -0,0 +1,41 @@
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// Resize the canvas when window is resized
canvas.resizeWorkspace = function () {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
window.addEventListener('resize', canvas.resizeWorkspace, false)
// Add elements to the document
document.body.appendChild(canvas)
canvas.resizeWorkspace()
ctx.imageSmoothingEnabled = false
class ResourceCacheFactory {
constructor () {
this.canvas = document.createElement('canvas')
this.ctx = this.canvas.getContext('2d')
this.ctx.imageSmoothingEnabled = false
}
prepare (width, height) {
if (width === this.canvas.width && height === this.canvas.height) {
this.ctx.clearRect(0, 0, width, height)
}
this.canvas.width = width
this.canvas.height = height
}
capture () {
let img = new window.Image()
img.src = this.canvas.toDataURL()
return img
}
}
export { canvas, ctx, ResourceCacheFactory }

49
src/debug.js Normal file
View File

@ -0,0 +1,49 @@
import { ctx } from './canvas'
class Debugging {
draw (vp, world, fps) {
let p = vp.chunkIn(world.chunkSize * world.tileSize)
ctx.fillStyle = '#fff'
ctx.fillText(fps + ' fps', 4, 16)
ctx.fillText('cam-in-chunk (x: ' + p.x + '; y: ' + p.y + ')', 4, 32)
ctx.fillText('loaded ' + world.chunks.length, 4, 48)
ctx.fillText('drawn ' + world._lastDrawCount, 4, 64)
ctx.fillText('updates ' + world._lastUpdateCount, 4, 80)
}
createSliders (obj, args, fn) {
let overlay = document.createElement('div')
overlay.style = 'color:#fff;position:absolute;top:15px;right:15px;background-color:hsla(0,0%,47%,0.5);padding:10px;display:flex;flex-direction:column;'
for (let a in args) {
let min = args[a][0]
let max = args[a][1]
let step = args[a][2]
let div = document.createElement('div')
div.style = 'display:flex;flex-direction:row;'
let name = document.createElement('span')
let value = document.createElement('value')
let slider = document.createElement('input')
slider.style = 'flex-grow:1;'
slider.type = 'range'
slider.min = min
slider.max = max
slider.step = step
slider.value = obj[a]
name.innerHTML = a
value.innerHTML = obj[a]
slider.addEventListener('input', function (e) {
obj[a] = parseFloat(slider.value)
value.innerHTML = slider.value
fn(a, slider.value)
})
div.appendChild(name)
div.appendChild(slider)
div.appendChild(value)
overlay.appendChild(div)
}
document.body.appendChild(overlay)
}
}
export default new Debugging()

90
src/heightmap.js Normal file
View File

@ -0,0 +1,90 @@
import seedrandom from 'seedrandom'
class PerlinNoise {
constructor (rand = Math.random) {
let perm = new Int8Array(257)
for (let i = 0; i < 256; i++) {
perm[i] = i & 1 ? 1 : -1
}
for (let i = 0; i < 256; i++) {
let j = rand() * 4294967296 & 255
var _ref = [perm[j], perm[i]]
perm[i] = _ref[0]
perm[j] = _ref[1]
}
perm[256] = perm[0]
function noise1d (x) {
let x0 = x | 0
let x1 = x - x0
let xi = x0 & 255
let fx = (3 - 2 * x1) * x1 * x1
let a = x1 * perm[xi]
let b = (x1 - 1) * perm[xi + 1]
return a + fx * (b - a)
}
function noise (x) {
let sum = 0
sum += (1 + noise1d(x)) * 0.25
sum += (1 + noise1d(x * 2)) * 0.125
sum += (1 + noise1d(x * 4)) * 0.0625
sum += (1 + noise1d(x * 8)) * 0.03125
return sum
}
this.noise = noise
}
}
class HeightMap {
// 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 (offsetX, offsetY, size, seed, amplitude = 30, persistence = 0.5, octaves = 5, period = 80, lacunarity = 2) {
this.ix = offsetX
this.iy = offsetY
this.seed = seed
this.size = size
this.noise = new PerlinNoise(seedrandom(seed))
this.amplitude = amplitude
this.period = period
this.lacunarity = lacunarity
this.octaves = octaves
this.persistence = persistence
}
getNoise (zx) {
let x = ((this.size * this.ix) + zx) / this.period
let amp = 1.0
let max = 1.0
let sum = this.noise.noise(x)
let i = 0
while (++i < this.octaves) {
x *= this.lacunarity
amp *= this.persistence
max += amp
sum += this.noise.noise(x) * amp
}
return sum / max
}
getHeight (x) {
return this.getNoise(x) * this.amplitude
}
}
export { HeightMap }

33
src/image.js Normal file
View File

@ -0,0 +1,33 @@
import Resource from './resource'
import { ctx } from './canvas'
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 (x, y, w, h) {
ctx.drawImage(this.img, x, y, w, h)
}
}
export { Image }

131
src/index.js Normal file
View File

@ -0,0 +1,131 @@
/* global requestAnimationFrame */
import { canvas, ctx } from './canvas'
import { TileMap, World } from './tiles'
import { HeightMap } from './heightmap'
import Debug from './debug'
import Input from './input'
import Viewport from './viewport'
import RES from './resource'
let playing = false
let frameTime = 0
let frameCount = 0
let fps = 0
let vp = new Viewport(0, 0)
let height = new HeightMap(0, 0, 16, 0)
let map = new TileMap('assets/ground.png', 32)
const chunkSize = 32
const tileSize = 12
// Define dirt tiles
map.define({
'DIRT_CORNER_TOP_LEFT': 0,
'DIRT_TOP': 1,
'DIRT_CORNER_TOP_RIGHT': 2,
'DIRT_INNER_BOTTOM_RIGHT': 3,
'DIRT_INNER_BOTTOM_LEFT': 4,
'DIRT_LEFT': 32,
'DIRT': 33,
'DIRT_RIGHT': 34,
'DIRT_INNER_TOP_RIGHT': 35,
'DIRT_INNER_TOP_LEFT': 36,
'DIRT_CORNER_BOTTOM_LEFT': 64,
'DIRT_BOTTOM': 65,
'DIRT_CORNER_BOTTOM_RIGHT': 66
})
// Define grass tiles
map.define({
'GRASS_CORNER_TOP_LEFT': 5,
'GRASS_TOP': 6,
'GRASS_CORNER_TOP_RIGHT': 7,
'GRASS_INNER_BOTTOM_RIGHT': 8,
'GRASS_INNER_BOTTOM_LEFT': 9,
'GRASS_LEFT': 37,
'GRASS_MID': 38,
'GRASS_RIGHT': 39,
'GRASS_INNER_TOP_RIGHT': 40,
'GRASS_INNER_TOP_LEFT': 41,
'GRASS_CORNER_BOTTOM_LEFT': 69,
'GRASS_BOTTOM': 70,
'GRASS_CORNER_BOTTOM_RIGHT': 71
})
map.define({
'AIR': -1,
'STONE': 10
})
let test = new World(height, { GROUND: map }, chunkSize, tileSize)
function update (dt) {
test.update(dt, vp)
if (Input.isDown('w')) {
vp.y -= 5
} else if (Input.isDown('s')) {
vp.y += 5
}
if (Input.isDown('a')) {
vp.x -= 5
} else if (Input.isDown('d')) {
vp.x += 5
}
}
function draw () {
test.draw(vp)
Debug.draw(vp, test, fps)
}
function step () {
draw()
let ts = window.performance.now()
let timeDiff = ts - frameTime // time difference in milliseconds
frameCount++
if (timeDiff > 0) {
fps = Math.floor(frameCount / timeDiff * 1000)
frameCount = 0
frameTime = ts
}
update(timeDiff / 1000)
}
function gameLoop () {
playing && requestAnimationFrame(gameLoop)
ctx.fillStyle = '#111'
ctx.fillRect(0, 0, canvas.width, canvas.height)
step()
Input.update()
}
function start () {
Debug.createSliders(height, {
amplitude: [0, 100, 1],
persistence: [0.1, 5, 0.1],
octaves: [1, 16, 1],
period: [1, 100, 1],
lacunarity: [1, 5, 1]
}, function (key, val) {
test.chunks = []
})
playing = true
gameLoop()
}
async function loadAll () {
let images = ['assets/ground.png']
for (let i in images) {
await RES.loadImage(images[i])
}
}
loadAll().then(start)

223
src/input.js Normal file
View File

@ -0,0 +1,223 @@
import { canvas } from './canvas'
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 new Input(canvas)

189
src/player.js Normal file
View File

@ -0,0 +1,189 @@
import { ctx } from './canvas'
import { GameObject } from './level'
import { deg2rad, rad2vec, intersectRect } from './utils'
import RES from './resource'
const FULL_ROTATION = -85
const FULL_ROTATION_EDGE = -FULL_ROTATION
class Hook extends GameObject {
constructor (player, x, y, w, h, len) {
super(x, y, w, h)
this.player = player
// Return position
this.rx = x
this.ry = y
// Hook rotation
// Hook rotation direction
this.r = 0
this.rd = 1
// Distance from center
// Moving direction
this.d = 0
this.md = -1
// Travel length
this.len = len
// Attached object
this.obj = null
}
draw () {
if (this.md !== -1) {
// Draw line
ctx.beginPath()
ctx.moveTo(ctx.oX + this.rx, ctx.oY + this.ry)
ctx.lineTo(ctx.oX + this.x, ctx.oY + this.y)
ctx.stroke()
ctx.closePath()
}
let hookr = deg2rad(360 - this.r)
ctx.save()
ctx.translate(ctx.oX + this.x, ctx.oY + this.y)
ctx.rotate(hookr)
ctx.drawImage(RES.loadImage('static/hook_open.png', true), -this.w / 2, -this.h / 2, this.w, this.h)
ctx.restore()
}
clear () {
this.obj = null
this.d = 0
this.md = -1
this.x = this.rx
this.y = this.ry
}
update (level) {
if (this.d === 0 && this.r < FULL_ROTATION_EDGE && this.rd === 1) {
this.r += 1
}
if (this.d === 0 && this.r > FULL_ROTATION && this.rd === 0) {
this.r -= 1
}
if (this.r >= FULL_ROTATION_EDGE && this.rd === 1) {
this.r = FULL_ROTATION_EDGE
this.rd = 0
}
if (this.r <= FULL_ROTATION && this.rd === 0) {
this.r = FULL_ROTATION
this.rd = 1
}
if (ctx.mouse.down['btn0'] && this.d === 0 && !this.obj) {
this.d = 0
this.md = 1
}
if (this.md > -1) {
if (this.d > this.len && this.md === 1) {
this.md = 0
}
if (this.d <= 2 && this.md === 0) {
this.d = 0
this.md = -1
this.x = this.rx
this.y = this.ry
// Score
if (this.obj) {
this.player.score(this.obj)
this.obj.destroy()
this.obj = null
}
return
}
let dir = rad2vec(deg2rad(90 - this.r))
dir.x *= this.d
dir.y *= this.d
this.x = this.rx + dir.x
this.y = this.ry + dir.y
if (this.obj) {
this.obj.x = this.x - this.obj.w / 2
this.obj.y = this.y
}
// Detect intersection
if (this.md === 1) {
if (!this.obj) {
let firstIntersect
for (let i in level.objects) {
let obj = level.objects[i]
if (obj.physical && intersectRect(obj, this)) {
firstIntersect = obj
break
}
}
if (firstIntersect) {
if (firstIntersect.explode) {
let obj = firstIntersect.explode(level)
this.obj = obj
this.md = 0
return
}
this.obj = firstIntersect
this.md = 0
return
}
}
if (this.player.superStrength) {
this.d += 10
} else {
this.d += 5
}
} else {
if (this.player.superStrength) {
this.d -= 10
} else {
this.d -= this.obj ? 5 * (1 - this.obj.weight) : 10
}
}
}
}
}
export class Player extends GameObject {
constructor (x, y, mh) {
super(x, y, 60, 55, '#4d4b4f')
this.x = x
this.y = y
this.hook = new Hook(this, this.x + this.w / 2, this.y + this.h, 20, 20, mh)
this.hook.r = FULL_ROTATION
this.superStrength = false
}
score (object) {
console.log('Scored', object)
}
draw () {
// Draw player
super.draw()
// Draw hook
this.hook.draw()
//
}
update (level) {
if (!level) return
this.hook.update(level)
}
}

103
src/resource.js Normal file
View File

@ -0,0 +1,103 @@
/* 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, onlyReturn = false, nowarn = false) {
if (onlyReturn && imgCache[url]) {
return imgCache[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 }

192
src/tiles.js Normal file
View File

@ -0,0 +1,192 @@
import { ctx, ResourceCacheFactory } from './canvas'
import Resource from './resource'
const cacheFactory = new ResourceCacheFactory()
class TileMap {
constructor (image, rows) {
this._src = image
this.rows = rows
this.defs = {}
}
get height () {
return this.image.height
}
get width () {
return this.image.width
}
get tile () {
return this.width / this.rows
}
get image () {
return Resource.loadImage(this._src, true)
}
tileAt (i) {
return { x: (i % this.rows) * this.tile, y: Math.floor(i / this.rows) * this.tile }
}
define (tile, index) {
if (typeof tile === 'object') {
this.defs = Object.assign(this.defs, tile)
return
}
this.defs[tile] = index
}
indexOf (tile) {
return this.defs[tile] || null
}
positionOf (tile) {
return this.tileAt(this.indexOf(tile))
}
}
class TileChunk {
constructor (ix, iy, size = 16, tileSize = 16) {
this.x = ix
this.y = iy
this.size = size
this.tile = tileSize
this.tiles = []
this.dirty = true
this.img = null
this._updated = false
}
generateMap (tileMap, heightMap) {
for (let i = 0; i < this.size * this.size; i++) {
let tileCoords = this.toXY(i)
let tileAbs = this.toAbs(tileCoords)
let y = Math.ceil(heightMap.getHeight(tileAbs.x) * 5 / 2) - 4
if (tileAbs.y < y) {
this.tiles.push(-1)
continue
}
if (tileAbs.y === y) {
this.tiles.push(tileMap.indexOf('GRASS_TOP'))
continue
}
if (tileAbs.y < y + 10) {
this.tiles.push(tileMap.indexOf('DIRT'))
continue
}
this.tiles.push(tileMap.indexOf('STONE'))
}
}
tileAt (i) {
return this.tiles[i]
}
tileAtXY (x, y) {
return this.tileAt(x + this.size * y)
}
toXY (i) {
return { x: i % this.size, y: Math.floor(i / this.size) }
}
toAbs (x, y) {
if (typeof x === 'object') {
y = x.y
x = x.x
}
return { x: this.x * this.size + x, y: this.y * this.size + y }
}
get absPos () {
return { x: this.x * this.size * this.tile, y: this.y * this.size * this.tile }
}
draw (view, map) {
// Create a cached image of the chunk
if (this.dirty || !this.img) {
cacheFactory.prepare(this.size * this.tile, this.size * this.tile)
for (let i in this.tiles) {
let tilei = this.tiles[i]
if (tilei === -1) continue
let coords = this.toXY(parseInt(i))
let tileCoords = map.tileAt(tilei)
cacheFactory.ctx.drawImage(map.image, tileCoords.x, tileCoords.y, map.tile, map.tile,
coords.x * this.tile, coords.y * this.tile, this.tile, this.tile)
}
this.img = cacheFactory.capture()
this.dirty = false
this._updated = true
return
}
// Draw the cached image
let p = this.absPos
ctx.drawImage(this.img, p.x - view.x, p.y - view.y)
}
update (dt) {
}
}
class World {
constructor (heightMap, tileMaps, chunkSize = 16, tileSize = 16) {
this.heightMap = heightMap
this.chunkSize = chunkSize
this.tileSize = tileSize
this.tileMaps = tileMaps
this.chunks = []
// Debug info
this._lastDrawCount = 0
this._lastUpdateCount = 0
}
getChunk (x, y) {
for (let i in this.chunks) {
let chunk = this.chunks[i]
if (chunk && chunk.x === x && chunk.y === y) return chunk
}
return null
}
update (dt, vp) {
let posPoint = vp.chunkIn(this.chunkSize * this.tileSize)
for (let x = posPoint.x - 4; x < posPoint.x + 5; x++) {
for (let y = posPoint.y - 4; y < posPoint.y + 5; y++) {
if (x < 0 || y < 0) continue
let exists = this.getChunk(x, y)
if (!exists) {
let n = new TileChunk(x, y, this.chunkSize, this.tileSize)
n.generateMap(this.tileMaps.GROUND, this.heightMap)
this.chunks.push(n)
break
}
}
}
for (let i in this.chunks) {
this.chunks[i].update(dt)
}
}
draw (vp) {
this._lastDrawCount = 0
this._lastUpdateCount = 0
for (let i in this.chunks) {
let chunk = this.chunks[i]
let absPos = chunk.absPos
let chunkSize = chunk.size * this.tileSize
if (absPos.x > vp.x + vp.width || absPos.x + chunkSize < vp.x ||
absPos.y > vp.y + vp.height || absPos.y + chunkSize < vp.y) continue
chunk._updated = false
chunk.draw(vp, this.tileMaps.GROUND)
if (chunk._updated) this._lastUpdateCount++
this._lastDrawCount++
}
}
}
export { TileMap, TileChunk, World }

29
src/utils.js Normal file
View File

@ -0,0 +1,29 @@
// Utility function
export function randomi (min, max) {
return Math.floor(Math.random() * (max - min) + min)
}
export function deg2rad (deg) {
return deg * Math.PI / 180
}
export function rad2vec (r) {
return { x: Math.cos(r), y: Math.sin(r) }
}
export function intersectRect (r1, r2) {
return !(r2.x > r1.w + r1.x ||
r2.w + r2.x < r1.x ||
r2.y > r1.h + r1.y ||
r2.h + r2.y < r1.y)
}
export function distanceTo (o1, o2) {
return Math.sqrt(Math.pow(o2.x - o1.x, 2) + Math.pow(o2.y - o1.y, 2))
}
export function mobile () {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
}

23
src/viewport.js Normal file
View File

@ -0,0 +1,23 @@
import { canvas } from './canvas'
class Viewport {
constructor (x, y, scale = 1) {
this.x = x
this.y = y
this.scale = scale
}
get width () {
return canvas.width
}
get height () {
return canvas.height
}
chunkIn (size) {
return { x: Math.floor((this.x + this.width / 2) / size), y: Math.floor((this.y + this.height / 2) / size) }
}
}
export default Viewport

30
webpack.config.js Normal file
View File

@ -0,0 +1,30 @@
const path = require('path')
const HtmlWebpackPlugin = require('html-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' })
]
}
}