diff --git a/assets/item_dirt.png b/assets/item_dirt.png new file mode 100644 index 0000000..8f809ce Binary files /dev/null and b/assets/item_dirt.png differ diff --git a/assets/item_grass.png b/assets/item_grass.png new file mode 100644 index 0000000..bb235af Binary files /dev/null and b/assets/item_grass.png differ diff --git a/assets/item_stone.png b/assets/item_stone.png new file mode 100644 index 0000000..ae33ce6 Binary files /dev/null and b/assets/item_stone.png differ diff --git a/src/debug.js b/src/debug.js index 4833ca6..b0a2b67 100644 --- a/src/debug.js +++ b/src/debug.js @@ -111,7 +111,7 @@ class Debugging { ctx.closePath() ctx.stroke() - // Draw colliders + /* Draw colliders ctx.fillStyle = '#00aaff' let collider = chunk.getLayer('col') for (let i in collider.tiles) { @@ -119,7 +119,7 @@ class Debugging { if (collider.tiles[i] === 0) continue ctx.fillRect(p.x * chunk.tile, p.y * chunk.tile, chunk.tile, chunk.tile) } - + */ // Chunk index ctx.fillStyle = '#fff' ctx.fillText(chunk.x + ';' + chunk.y, 5, 16) diff --git a/src/index.js b/src/index.js index cb3ebcf..7007899 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,8 @@ /* global requestAnimationFrame */ import { canvas, ctx } from './canvas' -import { TileMap, World } from './tiles' +import { Tile, TileMap, World } from './tiles' +import { ItemPlaceable } from './items' +import { Inventory } from './inventory' import { HeightMap } from './heightmap' import Debug from './debug' import Player from './player' @@ -15,6 +17,7 @@ let fps = 0 let vp = new Viewport(0, 0) let p = new Player(800, 1200, 32, 64) +let inv = new Inventory(9) let height = new HeightMap(0, 32, 16, 0) let map = new TileMap('assets/ground.png', 32) @@ -22,44 +25,56 @@ let map = new TileMap('assets/ground.png', 32) const chunkSize = 32 const tileSize = 16 +const dirtTile = new Tile('DIRT', 33) +const grassTile = new Tile('GRASS_TOP', 6) +const stoneTile = new Tile('STONE', 10) + +const dirtItem = new ItemPlaceable(dirtTile, 'dirt', 'assets/item_dirt.png') +const grassItem = new ItemPlaceable(grassTile, 'dirt_with_grass', 'assets/item_grass.png') +const stoneItem = new ItemPlaceable(stoneTile, 'stone', 'assets/item_stone.png') + +dirtTile.item = dirtItem +grassTile.item = grassItem +stoneTile.item = stoneItem + // 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 -}) +map.register([ + new Tile('DIRT_CORNER_TOP_LEFT', 0, true, dirtItem), + new Tile('DIRT_TOP', 1, true, dirtItem), + new Tile('DIRT_CORNER_TOP_RIGHT', 2, true, dirtItem), + new Tile('DIRT_INNER_BOTTOM_RIGHT', 3, true, dirtItem), + new Tile('DIRT_INNER_BOTTOM_LEFT', 4, true, dirtItem), + new Tile('DIRT_LEFT', 32, true, dirtItem), + dirtTile, + new Tile('DIRT_RIGHT', 34, true, dirtItem), + new Tile('DIRT_INNER_TOP_RIGHT', 35, true, dirtItem), + new Tile('DIRT_INNER_TOP_LEFT', 36, true, dirtItem), + new Tile('DIRT_CORNER_BOTTOM_LEFT', 64, true, dirtItem), + new Tile('DIRT_BOTTOM', 65, true, dirtItem), + new Tile('DIRT_CORNER_BOTTOM_RIGHT', 66, true, dirtItem) +]) // 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.register([ + new Tile('GRASS_CORNER_TOP_LEFT', 5, true, dirtItem), + grassTile, + new Tile('GRASS_CORNER_TOP_RIGHT', 7, true, dirtItem), + new Tile('GRASS_INNER_BOTTOM_RIGHT', 8, true, dirtItem), + new Tile('GRASS_INNER_BOTTOM_LEFT', 9, true, dirtItem), + new Tile('GRASS_LEFT', 37, true, dirtItem), + new Tile('GRASS_RIGHT', 39, true, dirtItem), + new Tile('GRASS_INNER_TOP_RIGHT', 40, true, dirtItem), + new Tile('GRASS_INNER_TOP_LEFT', 41, true, dirtItem), + new Tile('GRASS_CORNER_BOTTOM_LEFT', 69, true, dirtItem), + new Tile('GRASS_BOTTOM', 70, true, dirtItem), + new Tile('GRASS_CORNER_BOTTOM_RIGHT', 71, true, dirtItem) +]) -map.define({ - 'AIR': -1, - 'STONE': 10 -}) +// Define other tiles +map.register([ + new Tile('AIR', -1, false), + stoneTile +]) let world = new World(height, { GROUND: map }, chunkSize, tileSize, 32, 64) @@ -68,15 +83,36 @@ function update (dt) { p.update(dt, vp, world) vp.update(dt, world) + for (let i = 0; i < inv.size; i++) { + let pressed = Input.isPressed(i + 1) + if (pressed) { + inv.selected = i + break + } + } + if (Input.mouse['btn0']) { let mpin = world.pickMouse(vp, Input.mouse.pos) if (mpin.chunk) { - mpin.chunk.setTile('fg', mpin.tile, map.indexOf('DIRT')) + if (inv.isEmpty(inv.selected)) return + let tile = mpin.chunk.getTile('fg', mpin.tile) + if (tile !== -1) return + let itm = inv.getItem(inv.selected) + if (itm && itm.item.placeable) { + let success = mpin.chunk.setTile('fg', mpin.tile, itm.item.placeable.id) + if (success) { + inv.takeItem(inv.selected, 1) + } + } } } else if (Input.mouse['btn2']) { let mpin = world.pickMouse(vp, Input.mouse.pos) if (mpin.chunk) { - mpin.chunk.setTile('fg', mpin.tile, map.indexOf('AIR')) + let tile = mpin.chunk.getTile('fg', mpin.tile) + if (tile === -1) return + let itile = map.getTileByID(tile) + let success = mpin.chunk.setTile('fg', mpin.tile, map.indexOf('AIR')) + if (success) inv.addItem(itile.item) } } } @@ -85,6 +121,7 @@ function draw () { world.draw(vp) p.draw(vp) Debug.draw(vp, world, fps) + inv.draw() } function step () { @@ -131,7 +168,8 @@ function start () { } async function loadAll () { - let images = ['assets/ground.png'] + let images = ['assets/ground.png', 'assets/item_grass.png', + 'assets/item_dirt.png', 'assets/item_stone.png'] for (let i in images) { await RES.loadImage(images[i]) } diff --git a/src/inventory.js b/src/inventory.js new file mode 100644 index 0000000..4842988 --- /dev/null +++ b/src/inventory.js @@ -0,0 +1,89 @@ +import { canvas, ctx } from './canvas' +import { Item, ItemStack, MAX_STACK_SIZE } from './items' + +const SLOT_SIZE = 32 + +class Inventory { + constructor (size) { + this.size = size + this.items = [] + this.selected = 0 + } + + addItem (i) { + if (typeof i === 'string' || i instanceof Item) i = ItemStack.new(i) + let addedTo = false + let leftover = null + for (let k in this.items) { + if (addedTo) break + let itm = this.items[k] + if (itm.name === i.name || itm.isEmpty()) { + if (itm.isEmpty()) itm.item = i.item + let addedCount = itm.count + i.count + if (addedCount > MAX_STACK_SIZE) { + let m = addedCount - MAX_STACK_SIZE + let n = itm.copy() + n.count = m + itm.count = MAX_STACK_SIZE + if (this.items.length >= this.size) { + leftover = n + addedTo = true + } else { + continue + } + } else { + itm.count += i.count + addedTo = true + } + } + } + + if (!addedTo) { + if (this.items.length >= this.size) { + return i + } + this.items.push(i) + } + + return leftover + } + + getItem (slot) { + if (this.isEmpty(slot)) return null + return this.items[slot] + } + + takeItem (slot, count) { + if (this.isEmpty(slot)) return null + let i = this.items[slot] + if (!count || count > i.count) return i + i.count -= count + let copied = i.copy() + copied.count = count + return copied + } + + isEmpty (slot) { + if (!this.items[slot] || this.items[slot].isEmpty()) return true + return false + } + + draw () { + for (let i = 0; i < this.size; i++) { + let stack = this.items[i] + let x = canvas.width / 2 + i * (SLOT_SIZE + 8) - this.size / 2 * SLOT_SIZE + ctx.fillStyle = (this.selected === i) ? '#f00' : '#ddd' + ctx.fillRect(x, 16, SLOT_SIZE, SLOT_SIZE) + if (!stack || stack.isEmpty()) continue + ctx.drawImage(stack.item.image, x, 16, SLOT_SIZE, SLOT_SIZE) + ctx.font = '16px sans' + let measure = ctx.measureText(stack.count) + ctx.fillStyle = '#000' + ctx.fillText(stack.count, x + SLOT_SIZE / 2 - measure.width / 2, 8 + SLOT_SIZE) + ctx.fillStyle = '#fff' + ctx.fillText(stack.count, x + SLOT_SIZE / 2 - measure.width / 2 + 1, 8 + SLOT_SIZE + 1) + } + } +} + +export { Inventory } diff --git a/src/items.js b/src/items.js new file mode 100644 index 0000000..1dda81c --- /dev/null +++ b/src/items.js @@ -0,0 +1,87 @@ +import RES from './resource' + +const MAX_STACK_SIZE = 999 + +const ItemRegistry = new (class ItemRegistry { + constructor () { + this.items = {} + } + + register (name, item) { + this.items[name] = item + } + + get (name) { + return this.items[name] + } +})() + +class Item { + constructor (name, img, description) { + this.name = name + this._img = img + this.description = description + ItemRegistry.register(name, this) + } + + get image () { + return RES.loadImage(this._img, true) + } +} + +class ItemPlaceable extends Item { + constructor (tile, name, img, description) { + super(name, img, description) + this.placeable = tile + } +} + +class ItemStack { + static fromIString (str) { + if (typeof str !== 'string') return + let strpl = str.split(' ') + let iname = strpl[0] + let count = strpl[1] + let item = ItemRegistry.get(iname) + let istack = new ItemStack() + istack.item = item + istack.count = count || 1 + return istack + } + + static new (itemdef, count = 1, metadata) { + if (itemdef instanceof ItemStack) return itemdef.copy() + if (typeof itemdef === 'string') return ItemStack.fromIString(itemdef) + if (!(itemdef instanceof Item)) throw new Error('Invalid Item Definition!') + let istack = new ItemStack() + istack.item = itemdef + istack.count = count + istack.metadata = metadata + return istack + } + + copy () { + return ItemStack.new(this.item, this.count, this.metadata) + } + + get name () { + return this.item ? this.item.name : '' + } + + isEmpty () { + return this.item === null || this.count === 0 + } + + takeItem (c) { + let a = this.copy() + if (c > this.count) { + this.count = 0 + return a + } + this.count -= c + a.count = c + return a + } +} + +export { Item, ItemPlaceable, ItemStack, ItemRegistry, MAX_STACK_SIZE } diff --git a/src/player.js b/src/player.js index 4fea33b..fd41de2 100644 --- a/src/player.js +++ b/src/player.js @@ -14,7 +14,7 @@ class Player { this.grounded = false - this.speed = 5 + this.speed = 8 this.gravity = 1 this.jumpPower = 20 } @@ -35,8 +35,12 @@ class Player { let oldX = this.x this.x += this.mX if (oldX !== this.x && collider.collide(this)) { - this.x = oldX - this.mX = 0 + this.mX = this.mX < 0 ? -1 : 1 + this.x = oldX + this.mX + if (collider.collide(this)) { + this.x = oldX + this.mX = 0 + } } } diff --git a/src/tiles.js b/src/tiles.js index ffc2d59..40d27b4 100644 --- a/src/tiles.js +++ b/src/tiles.js @@ -6,11 +6,20 @@ import Debug from './debug' 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.defs = {} + this.tiles = [] } get height () { @@ -33,16 +42,41 @@ class TileMap { 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) + register (tile) { + if (typeof tile === 'object' && tile.length) { + for (let i in tile) { + this.register(tile[i]) + } return } - this.defs[tile] = index + 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) { - return this.defs[tile] || null + let t = this.getTileByName(tile) + return t ? t.id : null } positionOf (tile) { @@ -51,7 +85,8 @@ class TileMap { } class TileLayer { - constructor (name, size = 16, tileSize = 16) { + constructor (map, name, size = 16, tileSize = 16) { + this.map = map this.name = name this.size = size this.tile = tileSize @@ -70,11 +105,16 @@ class TileLayer { 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) } @@ -82,13 +122,13 @@ class TileLayer { return { x: i % this.size, y: Math.floor(i / this.size) } } - draw (ctx, view, map) { + 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 = map.tileAt(tilei) - ctx.drawImage(map.image, tileCoords.x, tileCoords.y, map.tile, map.tile, + 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) } @@ -113,7 +153,7 @@ class TileLayer { class TilePhysicsLayer extends TileLayer { constructor (size = 16, tileSize = 16) { - super('col', size, tileSize) + super(null, 'col', size, tileSize) this.empty = false } @@ -126,12 +166,15 @@ class TilePhysicsLayer extends TileLayer { this.empty = true for (let i in tiles.tiles) { let t = tiles.tiles[i] - let p = tiles.toXY(parseInt(i)) - if (t === -1) { + // 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) @@ -145,6 +188,7 @@ class TilePhysicsLayer extends TileLayer { this.empty = false this.tiles[i] = 1 + */ } } @@ -155,10 +199,10 @@ class TilePhysicsLayer extends TileLayer { 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 + 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 || @@ -185,8 +229,8 @@ class Chunk { generateMap (tileMap, heightMap) { this.layers = [] - let bgLayer = new TileLayer('bg', this.size, this.tile) - let fgLayer = new TileLayer('fg', this.size, this.tile) + let bgLayer = new TileLayer(tileMap, 'bg', this.size, this.tile) + let fgLayer = new TileLayer(tileMap, '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) @@ -214,7 +258,6 @@ class Chunk { fgLayer.tiles.push(tileMap.indexOf('STONE')) bgLayer.tiles.push(tileMap.indexOf('STONE')) } - clLayer.generateFromTiles(fgLayer) this.layers.push(bgLayer) this.layers.push(fgLayer) @@ -230,8 +273,18 @@ class Chunk { 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') { + if (!tile && typeof x === 'object') { tile = y y = x.y x = x.x @@ -260,7 +313,7 @@ class Chunk { return this.size * this.tile } - draw (view, map) { + draw (view) { if (this.img) { // Draw the cached image let p = this.absPos @@ -273,19 +326,19 @@ class Chunk { // Draw all layers for (let i in this.layers) { let layer = this.layers[i] - layer.draw(cacheFactory.ctx, view, map) + 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() - // 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 @@ -392,7 +445,7 @@ class World { 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) + chunk.draw(vp) if (chunk._updated) this._lastUpdateCount++ this._lastDrawCount++ } @@ -408,4 +461,4 @@ class World { } } -export { TileMap, Chunk, World } +export { Tile, TileMap, Chunk, World }