tilegame/src/tiles.js

412 lines
10 KiB
JavaScript

import { ctx, ResourceCacheFactory } from './canvas'
import { distanceTo } from './utils'
import Resource from './resource'
import Debug from './debug'
const cacheFactory = new ResourceCacheFactory()
const UPDATE_RADIUS = 6
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 TileLayer {
constructor (name, size = 16, tileSize = 16) {
this.name = name
this.size = size
this.tile = tileSize
this.tiles = []
}
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
}
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)
}
// 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
}
}
update (dt) {
}
}
class TilePhysicsLayer extends TileLayer {
constructor (size = 16, tileSize = 16) {
super('col', size, tileSize)
this.empty = false
}
draw () {}
update (dt) {}
generateFromTiles (tiles) {
this.tiles = []
this.empty = true
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
}
this.empty = false
this.tiles[i] = 1
}
}
collide (chunk, obj) {
if (this.empty) return false
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
}
}
class Chunk {
constructor (ix, iy, size = 16, tileSize = 16) {
this.x = ix
this.y = iy
this.size = size
this.tile = tileSize
this.layers = []
this.dirty = true
this.modified = false
this.img = null
this._updated = false
}
generateMap (tileMap, heightMap) {
this.layers = []
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)
for (let i = 0; i < this.size * this.size; i++) {
let tileCoords = fgLayer.toXY(i)
let tileAbs = this.toAbs(tileCoords)
let y = Math.ceil(heightMap.getHeight(tileAbs.x) * 5 / 2) - 4
if (tileAbs.y < y) {
fgLayer.tiles.push(-1)
bgLayer.tiles.push(-1)
continue
}
if (tileAbs.y === y) {
fgLayer.tiles.push(tileMap.indexOf('GRASS_TOP'))
bgLayer.tiles.push(tileMap.indexOf('DIRT'))
continue
}
if (tileAbs.y < y + 10) {
fgLayer.tiles.push(tileMap.indexOf('DIRT'))
bgLayer.tiles.push(tileMap.indexOf('DIRT'))
continue
}
if (tileAbs.y > heightMap.falloff - y + 64) {
fgLayer.tiles.push(-1)
continue
}
fgLayer.tiles.push(tileMap.indexOf('STONE'))
bgLayer.tiles.push(tileMap.indexOf('STONE'))
}
clLayer.generateFromTiles(fgLayer)
this.layers.push(bgLayer)
this.layers.push(fgLayer)
this.layers.push(clLayer)
this.dirty = true
}
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
}
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.fullSize, y: this.y * this.fullSize }
}
get fullSize () {
return this.size * this.tile
}
draw (view, map) {
if (this.img) {
// Draw the cached image
let p = this.absPos
ctx.drawImage(this.img, p.x - view.x, p.y - view.y)
}
// Create a cached image of the chunk
if (this.dirty || !this.img) {
cacheFactory.prepare(this.size * this.tile, this.size * this.tile)
// Draw all layers
for (let i in this.layers) {
let layer = this.layers[i]
layer.draw(cacheFactory.ctx, view, map)
}
// Draw a debug grid when enabled
Debug.chunkGrid(cacheFactory.ctx, this, view)
// Create cached image
this.img = cacheFactory.capture()
// Update collision
let cl = this.getLayer('col')
if (cl) cl.generateFromTiles(this.getLayer('fg'))
// Don't update again next tick
this.dirty = false
this._updated = true
}
}
update (dt) {
for (let i in this.layers) {
this.layers[i].update(dt)
}
}
collide (obj) {
let cl = this.getLayer('col')
if (!cl) return null
return cl.collide(this, obj)
}
}
class World {
constructor (heightMap, tileMaps, chunkSize = 16, tileSize = 16, height = 64, width = 128) {
this.heightMap = heightMap
this.chunkSize = chunkSize
this.tileSize = tileSize
this.tileMaps = tileMaps
this.chunks = []
this.height = height
this.width = width
// Indicate to the height map where the base of the world is
this.heightMap.falloff = height * this.chunkSize
// Debug info
this._unloadTick = 0
this._lastDrawCount = 0
this._lastUpdateCount = 0
this._active = []
}
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) {
this._active = []
let posPoint = vp.chunkIn(this.chunkSize * this.tileSize)
for (let x = posPoint.x - 3; x < posPoint.x + 4; x++) {
for (let y = posPoint.y - 2; y < posPoint.y + 3; y++) {
if (x < 0 || y < 0 || x >= this.width || y >= this.height) continue
let exists = this.getChunk(x, y)
if (!exists) {
let n = new Chunk(x, y, this.chunkSize, this.tileSize)
n.generateMap(this.tileMaps.GROUND, this.heightMap)
this.chunks.push(n)
continue
}
this._active.push(exists)
}
}
for (let i in this.chunks) {
this.chunks[i].update(dt)
}
// 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
}
}
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 }
}
draw (vp) {
this._lastDrawCount = 0
this._lastUpdateCount = 0
for (let i in this.chunks) {
let chunk = this.chunks[i]
let absPos = chunk.absPos
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
chunk._updated = false
chunk.draw(vp, this.tileMaps.GROUND)
if (chunk._updated) this._lastUpdateCount++
this._lastDrawCount++
}
}
collide (obj) {
if (!this._active.length) return null
for (let i in this._active) {
let c = this._active[i]
let collide = c.collide(obj)
if (collide) return collide
}
}
}
export { TileMap, Chunk, World }