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 }