simple inventory and item system

This commit is contained in:
Evert Prants 2020-01-11 00:54:37 +02:00
parent 71850fc7f9
commit 5a2ba57115
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
9 changed files with 344 additions and 73 deletions

BIN
assets/item_dirt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

BIN
assets/item_grass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

BIN
assets/item_stone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

View File

@ -111,7 +111,7 @@ class Debugging {
ctx.closePath() ctx.closePath()
ctx.stroke() ctx.stroke()
// Draw colliders /* Draw colliders
ctx.fillStyle = '#00aaff' ctx.fillStyle = '#00aaff'
let collider = chunk.getLayer('col') let collider = chunk.getLayer('col')
for (let i in collider.tiles) { for (let i in collider.tiles) {
@ -119,7 +119,7 @@ class Debugging {
if (collider.tiles[i] === 0) continue if (collider.tiles[i] === 0) continue
ctx.fillRect(p.x * chunk.tile, p.y * chunk.tile, chunk.tile, chunk.tile) ctx.fillRect(p.x * chunk.tile, p.y * chunk.tile, chunk.tile, chunk.tile)
} }
*/
// Chunk index // Chunk index
ctx.fillStyle = '#fff' ctx.fillStyle = '#fff'
ctx.fillText(chunk.x + ';' + chunk.y, 5, 16) ctx.fillText(chunk.x + ';' + chunk.y, 5, 16)

View File

@ -1,6 +1,8 @@
/* global requestAnimationFrame */ /* global requestAnimationFrame */
import { canvas, ctx } from './canvas' 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 { HeightMap } from './heightmap'
import Debug from './debug' import Debug from './debug'
import Player from './player' import Player from './player'
@ -15,6 +17,7 @@ let fps = 0
let vp = new Viewport(0, 0) let vp = new Viewport(0, 0)
let p = new Player(800, 1200, 32, 64) let p = new Player(800, 1200, 32, 64)
let inv = new Inventory(9)
let height = new HeightMap(0, 32, 16, 0) let height = new HeightMap(0, 32, 16, 0)
let map = new TileMap('assets/ground.png', 32) let map = new TileMap('assets/ground.png', 32)
@ -22,44 +25,56 @@ let map = new TileMap('assets/ground.png', 32)
const chunkSize = 32 const chunkSize = 32
const tileSize = 16 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 // Define dirt tiles
map.define({ map.register([
'DIRT_CORNER_TOP_LEFT': 0, new Tile('DIRT_CORNER_TOP_LEFT', 0, true, dirtItem),
'DIRT_TOP': 1, new Tile('DIRT_TOP', 1, true, dirtItem),
'DIRT_CORNER_TOP_RIGHT': 2, new Tile('DIRT_CORNER_TOP_RIGHT', 2, true, dirtItem),
'DIRT_INNER_BOTTOM_RIGHT': 3, new Tile('DIRT_INNER_BOTTOM_RIGHT', 3, true, dirtItem),
'DIRT_INNER_BOTTOM_LEFT': 4, new Tile('DIRT_INNER_BOTTOM_LEFT', 4, true, dirtItem),
'DIRT_LEFT': 32, new Tile('DIRT_LEFT', 32, true, dirtItem),
'DIRT': 33, dirtTile,
'DIRT_RIGHT': 34, new Tile('DIRT_RIGHT', 34, true, dirtItem),
'DIRT_INNER_TOP_RIGHT': 35, new Tile('DIRT_INNER_TOP_RIGHT', 35, true, dirtItem),
'DIRT_INNER_TOP_LEFT': 36, new Tile('DIRT_INNER_TOP_LEFT', 36, true, dirtItem),
'DIRT_CORNER_BOTTOM_LEFT': 64, new Tile('DIRT_CORNER_BOTTOM_LEFT', 64, true, dirtItem),
'DIRT_BOTTOM': 65, new Tile('DIRT_BOTTOM', 65, true, dirtItem),
'DIRT_CORNER_BOTTOM_RIGHT': 66 new Tile('DIRT_CORNER_BOTTOM_RIGHT', 66, true, dirtItem)
}) ])
// Define grass tiles // Define grass tiles
map.define({ map.register([
'GRASS_CORNER_TOP_LEFT': 5, new Tile('GRASS_CORNER_TOP_LEFT', 5, true, dirtItem),
'GRASS_TOP': 6, grassTile,
'GRASS_CORNER_TOP_RIGHT': 7, new Tile('GRASS_CORNER_TOP_RIGHT', 7, true, dirtItem),
'GRASS_INNER_BOTTOM_RIGHT': 8, new Tile('GRASS_INNER_BOTTOM_RIGHT', 8, true, dirtItem),
'GRASS_INNER_BOTTOM_LEFT': 9, new Tile('GRASS_INNER_BOTTOM_LEFT', 9, true, dirtItem),
'GRASS_LEFT': 37, new Tile('GRASS_LEFT', 37, true, dirtItem),
'GRASS_MID': 38, new Tile('GRASS_RIGHT', 39, true, dirtItem),
'GRASS_RIGHT': 39, new Tile('GRASS_INNER_TOP_RIGHT', 40, true, dirtItem),
'GRASS_INNER_TOP_RIGHT': 40, new Tile('GRASS_INNER_TOP_LEFT', 41, true, dirtItem),
'GRASS_INNER_TOP_LEFT': 41, new Tile('GRASS_CORNER_BOTTOM_LEFT', 69, true, dirtItem),
'GRASS_CORNER_BOTTOM_LEFT': 69, new Tile('GRASS_BOTTOM', 70, true, dirtItem),
'GRASS_BOTTOM': 70, new Tile('GRASS_CORNER_BOTTOM_RIGHT', 71, true, dirtItem)
'GRASS_CORNER_BOTTOM_RIGHT': 71 ])
})
map.define({ // Define other tiles
'AIR': -1, map.register([
'STONE': 10 new Tile('AIR', -1, false),
}) stoneTile
])
let world = new World(height, { GROUND: map }, chunkSize, tileSize, 32, 64) let world = new World(height, { GROUND: map }, chunkSize, tileSize, 32, 64)
@ -68,15 +83,36 @@ function update (dt) {
p.update(dt, vp, world) p.update(dt, vp, world)
vp.update(dt, 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']) { if (Input.mouse['btn0']) {
let mpin = world.pickMouse(vp, Input.mouse.pos) let mpin = world.pickMouse(vp, Input.mouse.pos)
if (mpin.chunk) { 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']) { } else if (Input.mouse['btn2']) {
let mpin = world.pickMouse(vp, Input.mouse.pos) let mpin = world.pickMouse(vp, Input.mouse.pos)
if (mpin.chunk) { 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) world.draw(vp)
p.draw(vp) p.draw(vp)
Debug.draw(vp, world, fps) Debug.draw(vp, world, fps)
inv.draw()
} }
function step () { function step () {
@ -131,7 +168,8 @@ function start () {
} }
async function loadAll () { 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) { for (let i in images) {
await RES.loadImage(images[i]) await RES.loadImage(images[i])
} }

89
src/inventory.js Normal file
View File

@ -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 }

87
src/items.js Normal file
View File

@ -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 }

View File

@ -14,7 +14,7 @@ class Player {
this.grounded = false this.grounded = false
this.speed = 5 this.speed = 8
this.gravity = 1 this.gravity = 1
this.jumpPower = 20 this.jumpPower = 20
} }
@ -35,10 +35,14 @@ class Player {
let oldX = this.x let oldX = this.x
this.x += this.mX this.x += this.mX
if (oldX !== this.x && collider.collide(this)) { if (oldX !== this.x && collider.collide(this)) {
this.mX = this.mX < 0 ? -1 : 1
this.x = oldX + this.mX
if (collider.collide(this)) {
this.x = oldX this.x = oldX
this.mX = 0 this.mX = 0
} }
} }
}
update (dt, vp, world) { update (dt, vp, world) {
this.mY += this.gravity this.mY += this.gravity

View File

@ -6,11 +6,20 @@ import Debug from './debug'
const cacheFactory = new ResourceCacheFactory() const cacheFactory = new ResourceCacheFactory()
const UPDATE_RADIUS = 6 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 { class TileMap {
constructor (image, rows) { constructor (image, rows) {
this._src = image this._src = image
this.rows = rows this.rows = rows
this.defs = {} this.tiles = []
} }
get height () { get height () {
@ -33,16 +42,41 @@ class TileMap {
return { x: (i % this.rows) * this.tile, y: Math.floor(i / this.rows) * this.tile } return { x: (i % this.rows) * this.tile, y: Math.floor(i / this.rows) * this.tile }
} }
define (tile, index) { register (tile) {
if (typeof tile === 'object') { if (typeof tile === 'object' && tile.length) {
this.defs = Object.assign(this.defs, tile) for (let i in tile) {
this.register(tile[i])
}
return 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) { indexOf (tile) {
return this.defs[tile] || null let t = this.getTileByName(tile)
return t ? t.id : null
} }
positionOf (tile) { positionOf (tile) {
@ -51,7 +85,8 @@ class TileMap {
} }
class TileLayer { class TileLayer {
constructor (name, size = 16, tileSize = 16) { constructor (map, name, size = 16, tileSize = 16) {
this.map = map
this.name = name this.name = name
this.size = size this.size = size
this.tile = tileSize this.tile = tileSize
@ -70,11 +105,16 @@ class TileLayer {
return true return true
} }
isSolid (i) {
return this.map.isSolid(i)
}
tileAt (i) { tileAt (i) {
return this.tiles[i] return this.tiles[i]
} }
tileAtXY (x, y) { tileAtXY (x, y) {
if (x < 0 || x >= this.size || y < 0 || y >= this.size) return null
return this.tileAt(x + this.size * y) return this.tileAt(x + this.size * y)
} }
@ -82,13 +122,13 @@ class TileLayer {
return { x: i % this.size, y: Math.floor(i / this.size) } return { x: i % this.size, y: Math.floor(i / this.size) }
} }
draw (ctx, view, map) { draw (ctx, view) {
for (let i in this.tiles) { for (let i in this.tiles) {
let tilei = this.tiles[i] let tilei = this.tiles[i]
if (tilei === -1) continue if (tilei === -1) continue
let coords = this.toXY(parseInt(i)) let coords = this.toXY(parseInt(i))
let tileCoords = map.tileAt(tilei) let tileCoords = this.map.tileAt(tilei)
ctx.drawImage(map.image, tileCoords.x, tileCoords.y, map.tile, map.tile, 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) coords.x * this.tile, coords.y * this.tile, this.tile, this.tile)
} }
@ -113,7 +153,7 @@ class TileLayer {
class TilePhysicsLayer extends TileLayer { class TilePhysicsLayer extends TileLayer {
constructor (size = 16, tileSize = 16) { constructor (size = 16, tileSize = 16) {
super('col', size, tileSize) super(null, 'col', size, tileSize)
this.empty = false this.empty = false
} }
@ -126,12 +166,15 @@ class TilePhysicsLayer extends TileLayer {
this.empty = true this.empty = true
for (let i in tiles.tiles) { for (let i in tiles.tiles) {
let t = tiles.tiles[i] let t = tiles.tiles[i]
let p = tiles.toXY(parseInt(i)) // let p = tiles.toXY(parseInt(i))
if (t === -1) { if (t === -1 || !tiles.isSolid(t)) {
this.tiles[i] = 0 this.tiles[i] = 0
continue 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 // 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 l = tiles.tileAtXY(p.x - 1, p.y)
let r = 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.empty = false
this.tiles[i] = 1 this.tiles[i] = 1
*/
} }
} }
@ -155,10 +199,10 @@ class TilePhysicsLayer extends TileLayer {
let t = this.tiles[i] let t = this.tiles[i]
if (t === 0) continue if (t === 0) continue
let p = this.toXY(parseInt(i)) let p = this.toXY(parseInt(i))
let minX = p.x * chunk.tile + absPos.x let minX = p.x * chunk.tile + absPos.x + 1
let minY = p.y * chunk.tile + absPos.y let minY = p.y * chunk.tile + absPos.y + 1
let maxX = minX + chunk.tile let maxX = minX + chunk.tile - 2
let maxY = minY + chunk.tile let maxY = minY + chunk.tile - 2
// Intersection check // Intersection check
if (minX > obj.x + obj.width || maxX < obj.x || if (minX > obj.x + obj.width || maxX < obj.x ||
@ -185,8 +229,8 @@ class Chunk {
generateMap (tileMap, heightMap) { generateMap (tileMap, heightMap) {
this.layers = [] this.layers = []
let bgLayer = new TileLayer('bg', this.size, this.tile) let bgLayer = new TileLayer(tileMap, 'bg', this.size, this.tile)
let fgLayer = new TileLayer('fg', this.size, this.tile) let fgLayer = new TileLayer(tileMap, 'fg', this.size, this.tile)
let clLayer = new TilePhysicsLayer(this.size, this.tile) let clLayer = new TilePhysicsLayer(this.size, this.tile)
for (let i = 0; i < this.size * this.size; i++) { for (let i = 0; i < this.size * this.size; i++) {
let tileCoords = fgLayer.toXY(i) let tileCoords = fgLayer.toXY(i)
@ -214,7 +258,6 @@ class Chunk {
fgLayer.tiles.push(tileMap.indexOf('STONE')) fgLayer.tiles.push(tileMap.indexOf('STONE'))
bgLayer.tiles.push(tileMap.indexOf('STONE')) bgLayer.tiles.push(tileMap.indexOf('STONE'))
} }
clLayer.generateFromTiles(fgLayer) clLayer.generateFromTiles(fgLayer)
this.layers.push(bgLayer) this.layers.push(bgLayer)
this.layers.push(fgLayer) this.layers.push(fgLayer)
@ -230,8 +273,18 @@ class Chunk {
return null 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) { setTile (layer, x, y, tile) {
if (!tile && typeof x !== 'object') { if (!tile && typeof x === 'object') {
tile = y tile = y
y = x.y y = x.y
x = x.x x = x.x
@ -260,7 +313,7 @@ class Chunk {
return this.size * this.tile return this.size * this.tile
} }
draw (view, map) { draw (view) {
if (this.img) { if (this.img) {
// Draw the cached image // Draw the cached image
let p = this.absPos let p = this.absPos
@ -273,19 +326,19 @@ class Chunk {
// Draw all layers // Draw all layers
for (let i in this.layers) { for (let i in this.layers) {
let layer = this.layers[i] 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 // Draw a debug grid when enabled
Debug.chunkGrid(cacheFactory.ctx, this, view) Debug.chunkGrid(cacheFactory.ctx, this, view)
// Create cached image // Create cached image
this.img = cacheFactory.capture() this.img = cacheFactory.capture()
// Update collision
let cl = this.getLayer('col')
if (cl) cl.generateFromTiles(this.getLayer('fg'))
// Don't update again next tick // Don't update again next tick
this.dirty = false this.dirty = false
this._updated = true 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 || 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 absPos.y > vp.y + vp.height + this.tileSize || absPos.y + chunk.fullSize < vp.y - this.tileSize) continue
chunk._updated = false chunk._updated = false
chunk.draw(vp, this.tileMaps.GROUND) chunk.draw(vp)
if (chunk._updated) this._lastUpdateCount++ if (chunk._updated) this._lastUpdateCount++
this._lastDrawCount++ this._lastDrawCount++
} }
@ -408,4 +461,4 @@ class World {
} }
} }
export { TileMap, Chunk, World } export { Tile, TileMap, Chunk, World }