2020-01-08 20:07:58 +00:00
|
|
|
import { ctx, ResourceCacheFactory } from './canvas'
|
2020-01-08 22:01:07 +00:00
|
|
|
import { distanceTo } from './utils'
|
2020-01-08 20:07:58 +00:00
|
|
|
import Resource from './resource'
|
2020-01-08 22:01:07 +00:00
|
|
|
import Debug from './debug'
|
2020-01-08 20:07:58 +00:00
|
|
|
|
|
|
|
const cacheFactory = new ResourceCacheFactory()
|
2020-01-08 22:01:07 +00:00
|
|
|
const UPDATE_RADIUS = 6
|
2020-01-08 20:07:58 +00:00
|
|
|
|
|
|
|
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))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-08 22:01:07 +00:00
|
|
|
class TileLayer {
|
2020-01-10 18:46:14 +00:00
|
|
|
constructor (name, size = 16, tileSize = 16) {
|
2020-01-08 22:01:07 +00:00
|
|
|
this.name = name
|
|
|
|
this.size = size
|
|
|
|
this.tile = tileSize
|
|
|
|
this.tiles = []
|
|
|
|
}
|
|
|
|
|
2020-01-09 14:37:18 +00:00
|
|
|
setTile (x, y, i) {
|
|
|
|
if (typeof x === 'object') {
|
|
|
|
if (!i && y) i = y
|
|
|
|
y = x.y
|
|
|
|
x = x.x
|
|
|
|
}
|
|
|
|
let t = this.tileAtXY(x, y)
|
|
|
|
if (!t || t === i) return false
|
|
|
|
this.tiles[x + this.size * y] = i
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2020-01-08 22:01:07 +00:00
|
|
|
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) }
|
|
|
|
}
|
|
|
|
|
|
|
|
draw (ctx, view, map) {
|
|
|
|
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)
|
|
|
|
ctx.drawImage(map.image, tileCoords.x, tileCoords.y, map.tile, map.tile,
|
|
|
|
coords.x * this.tile, coords.y * this.tile, this.tile, this.tile)
|
|
|
|
}
|
2020-01-10 19:10:38 +00:00
|
|
|
|
|
|
|
// Add some darkness to the BG layer
|
|
|
|
if (this.name === 'bg') {
|
|
|
|
ctx.globalAlpha = 0.3
|
|
|
|
ctx.fillStyle = '#000'
|
|
|
|
for (let i in this.tiles) {
|
|
|
|
let tilei = this.tiles[i]
|
|
|
|
if (tilei === -1) continue
|
|
|
|
let coords = this.toXY(parseInt(i))
|
|
|
|
ctx.fillRect(coords.x * this.tile, coords.y * this.tile, this.tile, this.tile)
|
|
|
|
}
|
|
|
|
ctx.globalAlpha = 1
|
|
|
|
}
|
2020-01-08 22:01:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
update (dt) {
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-10 18:46:14 +00:00
|
|
|
class TilePhysicsLayer extends TileLayer {
|
|
|
|
constructor (size = 16, tileSize = 16) {
|
|
|
|
super('col', size, tileSize)
|
2020-01-10 19:10:38 +00:00
|
|
|
this.empty = false
|
2020-01-10 18:46:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
draw () {}
|
|
|
|
|
|
|
|
update (dt) {}
|
|
|
|
|
|
|
|
generateFromTiles (tiles) {
|
|
|
|
this.tiles = []
|
2020-01-10 19:10:38 +00:00
|
|
|
this.empty = true
|
2020-01-10 18:46:14 +00:00
|
|
|
for (let i in tiles.tiles) {
|
|
|
|
let t = tiles.tiles[i]
|
|
|
|
let p = tiles.toXY(parseInt(i))
|
|
|
|
if (t === -1) {
|
|
|
|
this.tiles[i] = 0
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// If this tile has neighbors that are air but its not itself air, it has a collider
|
|
|
|
let l = tiles.tileAtXY(p.x - 1, p.y)
|
|
|
|
let r = tiles.tileAtXY(p.x + 1, p.y)
|
|
|
|
let u = tiles.tileAtXY(p.x, p.y - 1)
|
|
|
|
let d = tiles.tileAtXY(p.x, p.y + 1)
|
|
|
|
if ((l == null || l !== -1) && (r == null || r !== -1) &&
|
|
|
|
(u == null || u !== -1) && (d == null || d !== -1)) {
|
|
|
|
this.tiles[i] = 0
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-01-10 19:10:38 +00:00
|
|
|
this.empty = false
|
2020-01-10 18:46:14 +00:00
|
|
|
this.tiles[i] = 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
collide (chunk, obj) {
|
2020-01-10 19:10:38 +00:00
|
|
|
if (this.empty) return false
|
2020-01-10 18:46:14 +00:00
|
|
|
let absPos = chunk.absPos
|
|
|
|
for (let i in this.tiles) {
|
|
|
|
let t = this.tiles[i]
|
|
|
|
if (t === 0) continue
|
|
|
|
let p = this.toXY(parseInt(i))
|
|
|
|
let minX = p.x * chunk.tile + absPos.x
|
|
|
|
let minY = p.y * chunk.tile + absPos.y
|
|
|
|
let maxX = minX + chunk.tile
|
|
|
|
let maxY = minY + chunk.tile
|
|
|
|
|
|
|
|
// Intersection check
|
|
|
|
if (minX > obj.x + obj.width || maxX < obj.x ||
|
|
|
|
minY > obj.y + obj.height || maxY < obj.y) continue
|
|
|
|
|
|
|
|
return { chunk, tile: i }
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-08 22:01:07 +00:00
|
|
|
class Chunk {
|
2020-01-08 20:07:58 +00:00
|
|
|
constructor (ix, iy, size = 16, tileSize = 16) {
|
|
|
|
this.x = ix
|
|
|
|
this.y = iy
|
|
|
|
this.size = size
|
|
|
|
this.tile = tileSize
|
2020-01-08 22:01:07 +00:00
|
|
|
this.layers = []
|
2020-01-08 20:07:58 +00:00
|
|
|
this.dirty = true
|
2020-01-09 14:37:18 +00:00
|
|
|
this.modified = false
|
2020-01-08 20:07:58 +00:00
|
|
|
this.img = null
|
|
|
|
this._updated = false
|
|
|
|
}
|
|
|
|
|
|
|
|
generateMap (tileMap, heightMap) {
|
2020-01-08 22:01:07 +00:00
|
|
|
this.layers = []
|
2020-01-10 18:46:14 +00:00
|
|
|
let bgLayer = new TileLayer('bg', this.size, this.tile)
|
|
|
|
let fgLayer = new TileLayer('fg', this.size, this.tile)
|
|
|
|
let clLayer = new TilePhysicsLayer(this.size, this.tile)
|
2020-01-08 20:07:58 +00:00
|
|
|
for (let i = 0; i < this.size * this.size; i++) {
|
2020-01-08 22:01:07 +00:00
|
|
|
let tileCoords = fgLayer.toXY(i)
|
2020-01-08 20:07:58 +00:00
|
|
|
let tileAbs = this.toAbs(tileCoords)
|
|
|
|
let y = Math.ceil(heightMap.getHeight(tileAbs.x) * 5 / 2) - 4
|
|
|
|
if (tileAbs.y < y) {
|
2020-01-08 22:01:07 +00:00
|
|
|
fgLayer.tiles.push(-1)
|
2020-01-10 19:10:38 +00:00
|
|
|
bgLayer.tiles.push(-1)
|
2020-01-08 20:07:58 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
if (tileAbs.y === y) {
|
2020-01-08 22:01:07 +00:00
|
|
|
fgLayer.tiles.push(tileMap.indexOf('GRASS_TOP'))
|
2020-01-10 19:10:38 +00:00
|
|
|
bgLayer.tiles.push(tileMap.indexOf('DIRT'))
|
2020-01-08 20:07:58 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
if (tileAbs.y < y + 10) {
|
2020-01-08 22:01:07 +00:00
|
|
|
fgLayer.tiles.push(tileMap.indexOf('DIRT'))
|
2020-01-10 19:10:38 +00:00
|
|
|
bgLayer.tiles.push(tileMap.indexOf('DIRT'))
|
2020-01-08 20:07:58 +00:00
|
|
|
continue
|
|
|
|
}
|
2020-01-08 22:01:07 +00:00
|
|
|
if (tileAbs.y > heightMap.falloff - y + 64) {
|
|
|
|
fgLayer.tiles.push(-1)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
fgLayer.tiles.push(tileMap.indexOf('STONE'))
|
2020-01-10 19:10:38 +00:00
|
|
|
bgLayer.tiles.push(tileMap.indexOf('STONE'))
|
2020-01-08 20:07:58 +00:00
|
|
|
}
|
2020-01-10 18:46:14 +00:00
|
|
|
|
|
|
|
clLayer.generateFromTiles(fgLayer)
|
|
|
|
this.layers.push(bgLayer)
|
2020-01-08 22:01:07 +00:00
|
|
|
this.layers.push(fgLayer)
|
2020-01-10 18:46:14 +00:00
|
|
|
this.layers.push(clLayer)
|
2020-01-08 22:01:07 +00:00
|
|
|
this.dirty = true
|
2020-01-08 20:07:58 +00:00
|
|
|
}
|
|
|
|
|
2020-01-09 14:37:18 +00:00
|
|
|
getLayer (name) {
|
|
|
|
for (let i in this.layers) {
|
|
|
|
let layer = this.layers[i]
|
|
|
|
if (layer.name === name) return layer
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
setTile (layer, x, y, tile) {
|
|
|
|
if (!tile && typeof x !== 'object') {
|
|
|
|
tile = y
|
|
|
|
y = x.y
|
|
|
|
x = x.x
|
|
|
|
}
|
|
|
|
let l = this.getLayer(layer)
|
|
|
|
if (!l) return false
|
|
|
|
if (!l.setTile(x, y, tile)) return false
|
|
|
|
this.dirty = true
|
|
|
|
this.modified = true
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2020-01-08 20:07:58 +00:00
|
|
|
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 () {
|
2020-01-08 22:01:07 +00:00
|
|
|
return { x: this.x * this.fullSize, y: this.y * this.fullSize }
|
|
|
|
}
|
|
|
|
|
|
|
|
get fullSize () {
|
|
|
|
return this.size * this.tile
|
2020-01-08 20:07:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
draw (view, map) {
|
2020-01-09 14:37:18 +00:00
|
|
|
if (this.img) {
|
|
|
|
// Draw the cached image
|
|
|
|
let p = this.absPos
|
|
|
|
ctx.drawImage(this.img, p.x - view.x, p.y - view.y)
|
|
|
|
}
|
|
|
|
|
2020-01-08 20:07:58 +00:00
|
|
|
// Create a cached image of the chunk
|
|
|
|
if (this.dirty || !this.img) {
|
|
|
|
cacheFactory.prepare(this.size * this.tile, this.size * this.tile)
|
2020-01-08 22:01:07 +00:00
|
|
|
// Draw all layers
|
|
|
|
for (let i in this.layers) {
|
|
|
|
let layer = this.layers[i]
|
|
|
|
layer.draw(cacheFactory.ctx, view, map)
|
2020-01-08 20:07:58 +00:00
|
|
|
}
|
2020-01-08 22:01:07 +00:00
|
|
|
|
|
|
|
// Draw a debug grid when enabled
|
|
|
|
Debug.chunkGrid(cacheFactory.ctx, this, view)
|
|
|
|
|
|
|
|
// Create cached image
|
2020-01-08 20:07:58 +00:00
|
|
|
this.img = cacheFactory.capture()
|
2020-01-08 22:01:07 +00:00
|
|
|
|
2020-01-10 18:46:14 +00:00
|
|
|
// Update collision
|
|
|
|
let cl = this.getLayer('col')
|
|
|
|
if (cl) cl.generateFromTiles(this.getLayer('fg'))
|
|
|
|
|
2020-01-08 22:01:07 +00:00
|
|
|
// Don't update again next tick
|
2020-01-08 20:07:58 +00:00
|
|
|
this.dirty = false
|
|
|
|
this._updated = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
update (dt) {
|
2020-01-08 22:01:07 +00:00
|
|
|
for (let i in this.layers) {
|
|
|
|
this.layers[i].update(dt)
|
|
|
|
}
|
2020-01-08 20:07:58 +00:00
|
|
|
}
|
2020-01-10 18:46:14 +00:00
|
|
|
|
|
|
|
collide (obj) {
|
|
|
|
let cl = this.getLayer('col')
|
|
|
|
if (!cl) return null
|
|
|
|
return cl.collide(this, obj)
|
|
|
|
}
|
2020-01-08 20:07:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class World {
|
2020-01-08 22:01:07 +00:00
|
|
|
constructor (heightMap, tileMaps, chunkSize = 16, tileSize = 16, height = 64, width = 128) {
|
2020-01-08 20:07:58 +00:00
|
|
|
this.heightMap = heightMap
|
|
|
|
this.chunkSize = chunkSize
|
|
|
|
this.tileSize = tileSize
|
|
|
|
this.tileMaps = tileMaps
|
|
|
|
this.chunks = []
|
2020-01-08 22:01:07 +00:00
|
|
|
this.height = height
|
|
|
|
this.width = width
|
|
|
|
|
|
|
|
// Indicate to the height map where the base of the world is
|
|
|
|
this.heightMap.falloff = height * this.chunkSize
|
2020-01-08 20:07:58 +00:00
|
|
|
|
|
|
|
// Debug info
|
2020-01-08 22:01:07 +00:00
|
|
|
this._unloadTick = 0
|
2020-01-08 20:07:58 +00:00
|
|
|
this._lastDrawCount = 0
|
|
|
|
this._lastUpdateCount = 0
|
2020-01-10 19:10:38 +00:00
|
|
|
this._active = []
|
2020-01-08 20:07:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2020-01-10 19:10:38 +00:00
|
|
|
this._active = []
|
2020-01-08 20:07:58 +00:00
|
|
|
let posPoint = vp.chunkIn(this.chunkSize * this.tileSize)
|
2020-01-10 19:10:38 +00:00
|
|
|
for (let x = posPoint.x - 3; x < posPoint.x + 4; x++) {
|
|
|
|
for (let y = posPoint.y - 2; y < posPoint.y + 3; y++) {
|
2020-01-08 22:01:07 +00:00
|
|
|
if (x < 0 || y < 0 || x >= this.width || y >= this.height) continue
|
2020-01-08 20:07:58 +00:00
|
|
|
let exists = this.getChunk(x, y)
|
|
|
|
if (!exists) {
|
2020-01-08 22:01:07 +00:00
|
|
|
let n = new Chunk(x, y, this.chunkSize, this.tileSize)
|
2020-01-08 20:07:58 +00:00
|
|
|
n.generateMap(this.tileMaps.GROUND, this.heightMap)
|
|
|
|
this.chunks.push(n)
|
2020-01-10 18:46:14 +00:00
|
|
|
continue
|
2020-01-08 20:07:58 +00:00
|
|
|
}
|
2020-01-10 19:10:38 +00:00
|
|
|
this._active.push(exists)
|
2020-01-08 20:07:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i in this.chunks) {
|
|
|
|
this.chunks[i].update(dt)
|
|
|
|
}
|
2020-01-08 22:01:07 +00:00
|
|
|
|
|
|
|
// Remove far away chunks from memory
|
|
|
|
this._unloadTick++
|
|
|
|
if (this._unloadTick === 60) {
|
|
|
|
this._unloadTick = 0
|
|
|
|
let keep = []
|
|
|
|
for (let i in this.chunks) {
|
|
|
|
let chunk = this.chunks[i]
|
|
|
|
let pos = chunk.absPos
|
|
|
|
let distance = distanceTo(vp.adjustCentered, pos)
|
|
|
|
if (distance <= chunk.fullSize * UPDATE_RADIUS) {
|
|
|
|
// Keep chunk
|
|
|
|
keep.push(chunk)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.chunks = keep
|
|
|
|
}
|
2020-01-08 20:07:58 +00:00
|
|
|
}
|
|
|
|
|
2020-01-09 14:37:18 +00:00
|
|
|
pickMouse (vp, mousePos) {
|
|
|
|
let a = this.chunkSize * this.tileSize
|
|
|
|
let abs = { x: mousePos.x + vp.x, y: mousePos.y + vp.y }
|
|
|
|
let chunk = { x: Math.floor(abs.x / a), y: Math.floor(abs.y / a) }
|
|
|
|
let tile = {
|
|
|
|
x: Math.floor(abs.x / this.tileSize - chunk.x * this.chunkSize),
|
|
|
|
y: Math.floor(abs.y / this.tileSize - chunk.y * this.chunkSize)
|
|
|
|
}
|
|
|
|
return { chunk: this.getChunk(chunk.x, chunk.y), tile }
|
|
|
|
}
|
|
|
|
|
2020-01-08 20:07:58 +00:00
|
|
|
draw (vp) {
|
|
|
|
this._lastDrawCount = 0
|
|
|
|
this._lastUpdateCount = 0
|
|
|
|
for (let i in this.chunks) {
|
|
|
|
let chunk = this.chunks[i]
|
|
|
|
let absPos = chunk.absPos
|
2020-01-09 14:37:18 +00:00
|
|
|
if (absPos.x > vp.x + vp.width + this.tileSize || absPos.x + chunk.fullSize < vp.x - this.tileSize ||
|
|
|
|
absPos.y > vp.y + vp.height + this.tileSize || absPos.y + chunk.fullSize < vp.y - this.tileSize) continue
|
2020-01-08 20:07:58 +00:00
|
|
|
chunk._updated = false
|
|
|
|
chunk.draw(vp, this.tileMaps.GROUND)
|
|
|
|
if (chunk._updated) this._lastUpdateCount++
|
|
|
|
this._lastDrawCount++
|
|
|
|
}
|
|
|
|
}
|
2020-01-10 18:46:14 +00:00
|
|
|
|
|
|
|
collide (obj) {
|
2020-01-10 19:10:38 +00:00
|
|
|
if (!this._active.length) return null
|
|
|
|
for (let i in this._active) {
|
|
|
|
let c = this._active[i]
|
2020-01-10 18:46:14 +00:00
|
|
|
let collide = c.collide(obj)
|
|
|
|
if (collide) return collide
|
|
|
|
}
|
|
|
|
}
|
2020-01-08 20:07:58 +00:00
|
|
|
}
|
|
|
|
|
2020-01-08 22:01:07 +00:00
|
|
|
export { TileMap, Chunk, World }
|