tilegame/src/tiles.js

499 lines
12 KiB
JavaScript

import { ctx, ResourceCacheFactory } from './canvas'
import { distanceTo } from './utils'
import Resource from './resource'
import Debug from './debug'
import { EntityLayer } from './entity'
const cacheFactory = new ResourceCacheFactory()
const UPDATE_RADIUS = 6
class Tile {
constructor (name, index, solid = true, item) {
this.name = name
this.id = index
this.solid = solid
this.item = item
}
}
class TileMap {
constructor (image, rows) {
this._src = image
this.rows = rows
this.tiles = []
}
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 }
}
register (tile) {
if (typeof tile === 'object' && tile.length) {
for (let i in tile) {
this.register(tile[i])
}
return
}
if (!(tile instanceof Tile)) return
this.tiles.push(tile)
}
getTileByName (tile) {
for (let i in this.tiles) {
let t = this.tiles[i]
if (t.name === tile) return t
}
return null
}
getTileByID (id) {
for (let i in this.tiles) {
let t = this.tiles[i]
if (t.id === id) return t
}
return null
}
isSolid (id) {
let t = this.getTileByID(id)
return t ? t.solid : false
}
indexOf (tile) {
let t = this.getTileByName(tile)
return t ? t.id : null
}
positionOf (tile) {
return this.tileAt(this.indexOf(tile))
}
}
class TileLayer {
constructor (map, name, size = 16, tileSize = 16) {
this.map = map
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
}
isSolid (i) {
return this.map.isSolid(i)
}
tileAt (i) {
return this.tiles[i]
}
tileAtXY (x, y) {
if (x < 0 || x >= this.size || y < 0 || y >= this.size) return null
return this.tileAt(x + this.size * y)
}
toXY (i) {
return { x: i % this.size, y: Math.floor(i / this.size) }
}
draw (ctx, view) {
for (let i in this.tiles) {
let tilei = this.tiles[i]
if (tilei === -1) continue
let coords = this.toXY(parseInt(i))
let tileCoords = this.map.tileAt(tilei)
ctx.drawImage(this.map.image, tileCoords.x, tileCoords.y, this.map.tile, this.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 TileCollisionLayer extends TileLayer {
constructor (size = 16, tileSize = 16) {
super(null, '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 || !tiles.isSolid(t)) {
this.tiles[i] = 0
continue
}
this.empty = false
this.tiles[i] = 1
/*
// Surface tiles only
// 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 + 1
let minY = p.y * chunk.tile + absPos.y + 1
let maxX = minX + chunk.tile - 2
let maxY = minY + chunk.tile - 2
// 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(tileMap, 'bg', this.size, this.tile)
let fgLayer = new TileLayer(tileMap, 'fg', this.size, this.tile)
let clLayer = new TileCollisionLayer(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
}
getTile (layer, x, y) {
if (typeof x === 'object') {
y = x.y
x = x.x
}
let l = this.getLayer(layer)
if (!l) return null
return l.tileAtXY(x, y)
}
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) {
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)
}
// Update collision
let cl = this.getLayer('col')
if (cl) cl.generateFromTiles(this.getLayer('fg'))
// Draw a debug grid when enabled
Debug.chunkGrid(cacheFactory.ctx, this, view)
// Create cached image
this.img = cacheFactory.capture()
// 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.layers = []
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 = []
// Create world layers
this.layers.push(new EntityLayer('ents'))
}
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
}
getLayer (name) {
for (let i in this.layers) {
let layer = this.layers[i]
if (layer.name === name) return layer
}
return null
}
update (dt, vp) {
this._active = []
let posPoint = vp.chunkIn(this.chunkSize * this.tileSize)
// Load chunks if necessary
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)
}
}
// Update chunks
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 || chunk.modified) {
// Keep chunk
keep.push(chunk)
}
}
this.chunks = keep
}
// Update layers
for (let i in this.layers) {
this.layers[i].update(dt, vp, this)
}
}
// Get a position in the world grid from an absolute position
gridPosition (pos) {
let a = this.chunkSize * this.tileSize
let chunk = { x: Math.floor(pos.x / a), y: Math.floor(pos.y / a) }
let tile = {
x: Math.floor(pos.x / this.tileSize - chunk.x * this.chunkSize),
y: Math.floor(pos.y / this.tileSize - chunk.y * this.chunkSize)
}
return { chunk: this.getChunk(chunk.x, chunk.y), tile }
}
// Forces all loaded chunks to re-draw themselves
redraw () {
for (let i in this.chunks) {
this.chunks[i].dirty = true
}
}
// Draw all loaded in-view chunks and special layers
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)
if (chunk._updated) this._lastUpdateCount++
this._lastDrawCount++
}
// Draw layers
for (let i in this.layers) {
this.layers[i].draw(vp, this)
}
}
// Terrain collision test
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 { Tile, TileMap, Chunk, World }