item entities

This commit is contained in:
Evert Prants 2020-01-11 13:11:34 +02:00
parent 5a2ba57115
commit 71eaa328a4
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
7 changed files with 395 additions and 133 deletions

View File

@ -24,7 +24,8 @@ class Debugging {
ctx.fillText('active ' + world._active.length, 4, 16 * 7) ctx.fillText('active ' + world._active.length, 4, 16 * 7)
// Mouse // Mouse
let mpos = Input.mouse.pos let mpos = Input.mouse.pos
let mpin = world.pickMouse(vp, mpos) let mabs = { x: mpos.x + vp.x, y: mpos.y + vp.y }
let mpin = world.gridPosition(mabs)
ctx.fillText('mouse (x: ' + mpos.x + '; y: ' + mpos.y + ')', 4, 16 * 9) ctx.fillText('mouse (x: ' + mpos.x + '; y: ' + mpos.y + ')', 4, 16 * 9)
if (mpin.chunk) { if (mpin.chunk) {
ctx.fillText('mouse-in-chunk (x: ' + mpin.chunk.x + '; y: ' + mpin.chunk.y + ')', 4, 16 * 10) ctx.fillText('mouse-in-chunk (x: ' + mpin.chunk.x + '; y: ' + mpin.chunk.y + ')', 4, 16 * 10)

214
src/entity.js Normal file
View File

@ -0,0 +1,214 @@
import { ctx } from './canvas'
import { ItemRegistry, ItemStack } from './items'
class PhysicsEntity {
constructor (x, y, w, h) {
this.x = x
this.y = y
this.width = w
this.height = h
this.mX = 0
this.mY = 0
this.grounded = false
this.speed = 8
this.gravity = 1
this.jumpPower = 20
this.dead = false
}
moveAndSlide (collider) {
// y collision
if (this.mY !== 0) {
let oldY = this.y
this.y += this.mY
if (oldY !== this.y && collider.collide(this)) {
if (this.y > oldY) this.grounded = true
this.y = oldY
this.mY = 0
} else {
this.grounded = false
}
}
// x collision
if (this.mX !== 0) {
let oldX = this.x
this.x += this.mX
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.mX = 0
}
}
}
}
update (dt, vp, world) {
this.mY += this.gravity
this.moveAndSlide(world)
}
draw (vp) {
ctx.fillStyle = '#f00'
ctx.fillRect(this.x - vp.x, this.y - vp.y, this.width, this.height)
}
collide (ent) {
if (!(ent instanceof PhysicsEntity)) return null
// Intersection check
if (this.x > ent.x + ent.width || this.x + this.width < ent.x ||
this.y > ent.y + ent.height || this.y + this.height < ent.y) return false
return true
}
distance (ent) {
if (!(ent instanceof PhysicsEntity)) return null
let d1 = { x: ent.x + ent.width / 2, y: ent.y + ent.height / 2 }
let d2 = { x: this.x + this.width / 2, y: this.y + this.height / 2 }
let dist = Math.floor(Math.sqrt(Math.pow(d1.x - d2.x, 2) + Math.pow(d1.y - d2.y, 2)))
return dist
}
}
const ITEM_LIFE = 120
const ITEM_MERGE_DISTANCE = 60
const ITEM_BOB_FACTOR = 5
class ItemEntity extends PhysicsEntity {
constructor (istr, x, y) {
super(x, y, 16, 16)
this.istr = istr
this._mergeTick = 0
this._life = 0
}
get name () {
if (this.istr === '') return ''
return this.istr.split(' ')[0]
}
get count () {
if (this.istr === '') return ''
let c = parseInt(this.istr.split(' ')[1])
return isNaN(c) ? 0 : c
}
get item () {
let itm = ItemRegistry.get(this.name)
return itm
}
static new (itemStack, x, y) {
if (!(itemStack instanceof ItemStack)) return null
return new ItemEntity(itemStack.toString(), x, y)
}
mergeNearby (vp, world) {
let entLayer = world.getLayer('ents')
if (!entLayer) return
let active = entLayer.getActiveEntities(vp, world)
for (let i in active) {
let ent = active[i]
if (ent.dead) continue
if (ent === this) continue
if (!(ent instanceof ItemEntity)) continue
if (ent.name !== this.name) continue
let dist = Math.floor(Math.sqrt(Math.pow(ent.x - this.x, 2) + Math.pow(ent.y - this.y, 2)))
if (dist > ITEM_MERGE_DISTANCE) continue
this.istr = this.name + ' ' + (this.count + ent.count)
this._life = 0
ent.dead = true
continue
}
}
update (dt, vp, world) {
if (this.dead) return
this._mergeTick++
this._life += 1 * dt
if (this._mergeTick >= 60) {
this._mergeTick = 0
this.mergeNearby(vp, world)
}
if (this._life > ITEM_LIFE) {
this.dead = true
return
}
super.update(dt, vp, world)
}
draw (vp, world) {
let i = this.item
let b = Math.sin((this._life / ITEM_BOB_FACTOR) / Math.PI * 180)
if (!i) return
ctx.drawImage(i.image, this.x - vp.x, this.y - vp.y + b, this.width, this.height)
}
}
class EntityLayer {
constructor (name) {
this.name = name
this.entities = []
}
getActiveEntities (vp, world, cleanup = false) {
let active = []
let alive = []
for (let i in this.entities) {
let ent = this.entities[i]
if (ent.dead) {
continue
} else if (cleanup) {
alive.push(ent)
}
let entInChunk = world.gridPosition(vp, ent)
if (!entInChunk || !entInChunk.chunk) continue
let entChunk
for (let i in world._active) {
let chunk = world._active[i]
if (chunk.x === entInChunk.chunk.x && chunk.y === entInChunk.chunk.y) {
entChunk = chunk
break
}
}
if (!entChunk) continue
ent._chunk = entChunk
active.push(ent)
}
if (cleanup) this.entities = alive
return active
}
// Update active entities and clean up dead ones
update (dt, vp, world) {
let active = this.getActiveEntities(vp, world, true)
for (let i in active) {
let ent = active[i]
ent.update(dt, vp, world, ent._chunk)
}
}
draw (vp, world) {
let active = this.getActiveEntities(vp, world)
for (let i in active) {
let ent = active[i]
if (ent.x > vp.x + vp.width + ent.width || ent.x + ent._chunk.fullSize < vp.x - ent.width ||
ent.y > vp.y + vp.height + ent.height || ent.y + ent._chunk.fullSize < vp.y - ent.height) continue
ent.draw(vp, world)
}
}
}
export { ItemEntity, PhysicsEntity, EntityLayer }

View File

@ -1,14 +1,14 @@
/* global requestAnimationFrame */ /* global requestAnimationFrame */
import { canvas, ctx } from './canvas' import { canvas, ctx } from './canvas'
import { Tile, TileMap, World } from './tiles' import { World } from './tiles'
import { ItemPlaceable } from './items' // 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'
import Input from './input' import Input from './input'
import Viewport from './viewport' import Viewport from './viewport'
import RES from './resource' import RES from './resource'
import map from './register'
let playing = false let playing = false
let frameTime = 0 let frameTime = 0
@ -17,111 +17,24 @@ 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)
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
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.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)
])
// Define other tiles
map.register([
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)
function update (dt) { function update (dt) {
world.update(dt, vp) world.update(dt, vp)
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']) {
let mpin = world.pickMouse(vp, Input.mouse.pos)
if (mpin.chunk) {
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) {
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)
}
}
} }
function draw () { 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 () {

View File

@ -36,16 +36,22 @@ class ItemPlaceable extends Item {
} }
} }
class ItemTool extends Item {
use (dt, world, player) {}
useSecondary (dt, world, player) {}
}
class ItemStack { class ItemStack {
static fromIString (str) { static fromIString (str) {
if (typeof str !== 'string') return if (typeof str !== 'string') return
let strpl = str.split(' ') let strpl = str.split(' ')
let iname = strpl[0] let iname = strpl[0]
let count = strpl[1] let count = parseInt(strpl[1])
let item = ItemRegistry.get(iname) let item = ItemRegistry.get(iname)
let istack = new ItemStack() let istack = new ItemStack()
istack.item = item istack.item = item
istack.count = count || 1 istack.count = isNaN(count) ? 1 : count
return istack return istack
} }
@ -82,6 +88,10 @@ class ItemStack {
a.count = c a.count = c
return a return a
} }
toString () {
return this.name + ' ' + this.count + (this.metadata ? ' ' + JSON.stringify(this.metadata) : '')
}
} }
export { Item, ItemPlaceable, ItemStack, ItemRegistry, MAX_STACK_SIZE } export { Item, ItemPlaceable, ItemTool, ItemStack, ItemRegistry, MAX_STACK_SIZE }

View File

@ -1,50 +1,54 @@
import { ctx } from './canvas' import { ctx } from './canvas'
import { PhysicsEntity, ItemEntity } from './entity'
import { ItemStack } from './items'
import { Inventory } from './inventory'
import Input from './input' import Input from './input'
class Player { class Player extends PhysicsEntity {
constructor (x, y, w, h) { constructor (x, y, w, h) {
this.x = x super(x, y, w, h)
this.y = y
this.width = w
this.height = h
this.mX = 0
this.mY = 0
this.grounded = false
this.speed = 8 this.speed = 8
this.gravity = 1 this.gravity = 1
this.jumpPower = 20 this.jumpPower = 20
this.inv = new Inventory(9)
this.itemPickUpDistance = 40
} }
moveAndSlide (collider) { handleTool (dt, vp, world) {
// y collision let mabs = { x: Input.mouse.pos.x + vp.x, y: Input.mouse.pos.y + vp.y }
let oldY = this.y let mpin = world.gridPosition(mabs)
this.y += this.mY if (Input.mouse['btn0']) {
if (oldY !== this.y && collider.collide(this)) { if (mpin.chunk) {
if (this.y > oldY) this.grounded = true if (this.inv.isEmpty(this.inv.selected)) return
this.y = oldY let tile = mpin.chunk.getTile('fg', mpin.tile)
this.mY = 0 if (tile !== -1) return
} else { let itm = this.inv.getItem(this.inv.selected)
this.grounded = false if (itm && itm.item.placeable) {
} let success = mpin.chunk.setTile('fg', mpin.tile, itm.item.placeable.id)
if (success) {
// x collision this.inv.takeItem(this.inv.selected, 1)
let oldX = this.x }
this.x += this.mX }
if (oldX !== this.x && collider.collide(this)) { }
this.mX = this.mX < 0 ? -1 : 1 } else if (Input.mouse['btn2']) {
this.x = oldX + this.mX if (mpin.chunk) {
if (collider.collide(this)) { let layer = mpin.chunk.getLayer('fg')
this.x = oldX let tile = layer.tileAtXY(mpin.tile.x, mpin.tile.y)
this.mX = 0 if (tile === -1) return
let itile = layer.map.getTileByID(tile)
let success = mpin.chunk.setTile('fg', mpin.tile, layer.map.indexOf('AIR'))
if (success) {
let e = ItemEntity.new(ItemStack.new(itile.item, 1), mabs.x - 8, mabs.y - 8)
let p = world.getLayer('ents')
p.entities.push(e)
}
} }
} }
} }
update (dt, vp, world) { handleMovement (dt, vp, world) {
this.mY += this.gravity this.mY += this.gravity
if (Input.isDown('a')) { if (Input.isDown('a')) {
this.mX = -this.speed this.mX = -this.speed
@ -64,9 +68,46 @@ class Player {
vp.y = parseInt(this.y - vp.height / 2) vp.y = parseInt(this.y - vp.height / 2)
} }
handleInventory () {
for (let i = 0; i < this.inv.size; i++) {
let pressed = Input.isPressed(i + 1)
if (pressed) {
this.inv.selected = i
break
}
}
}
pickUp (itemEntity) {
let istr = itemEntity.istr
let istack = ItemStack.new(istr)
this.inv.addItem(istack)
itemEntity.dead = true
}
handleWorldEntities (dt, vp, world) {
let entities = world.getLayer('ents')
if (!entities) return
let active = entities.getActiveEntities(vp, world)
for (let i in active) {
let ent = active[i]
if (!(ent instanceof ItemEntity)) continue
if (ent.distance(this) > this.itemPickUpDistance) continue
this.pickUp(ent)
}
}
update (dt, vp, world) {
this.handleTool(dt, vp, world)
this.handleWorldEntities(dt, vp, world)
this.handleInventory()
this.handleMovement(dt, vp, world)
}
draw (vp) { draw (vp) {
ctx.fillStyle = '#f00' ctx.fillStyle = '#f00'
ctx.fillRect(this.x - vp.x, this.y - vp.y, this.width, this.height) ctx.fillRect(this.x - vp.x, this.y - vp.y, this.width, this.height)
this.inv.draw()
} }
} }

60
src/register.js Normal file
View File

@ -0,0 +1,60 @@
import { Tile, TileMap } from './tiles'
import { ItemPlaceable } from './items'
const map = new TileMap('assets/ground.png', 32)
// Basic tiles
const dirtTile = new Tile('DIRT', 33)
const grassTile = new Tile('GRASS_TOP', 6)
const stoneTile = new Tile('STONE', 10)
// Items for basic tiles
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')
// Set the items
dirtTile.item = dirtItem
grassTile.item = grassItem
stoneTile.item = stoneItem
// Register dirt tiles
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)
])
// Register grass tiles
map.register([
new Tile('GRASS_CORNER_TOP_LEFT', 5, true, grassItem),
grassTile,
new Tile('GRASS_CORNER_TOP_RIGHT', 7, true, grassItem),
new Tile('GRASS_INNER_BOTTOM_RIGHT', 8, true, grassItem),
new Tile('GRASS_INNER_BOTTOM_LEFT', 9, true, grassItem),
new Tile('GRASS_LEFT', 37, true, grassItem),
new Tile('GRASS_RIGHT', 39, true, grassItem),
new Tile('GRASS_INNER_TOP_RIGHT', 40, true, grassItem),
new Tile('GRASS_INNER_TOP_LEFT', 41, true, grassItem),
new Tile('GRASS_CORNER_BOTTOM_LEFT', 69, true, grassItem),
new Tile('GRASS_BOTTOM', 70, true, grassItem),
new Tile('GRASS_CORNER_BOTTOM_RIGHT', 71, true, grassItem)
])
// Register other tiles
map.register([
new Tile('AIR', -1, false),
stoneTile
])
export default map

View File

@ -2,6 +2,7 @@ import { ctx, ResourceCacheFactory } from './canvas'
import { distanceTo } from './utils' import { distanceTo } from './utils'
import Resource from './resource' import Resource from './resource'
import Debug from './debug' import Debug from './debug'
import { EntityLayer } from './entity'
const cacheFactory = new ResourceCacheFactory() const cacheFactory = new ResourceCacheFactory()
const UPDATE_RADIUS = 6 const UPDATE_RADIUS = 6
@ -365,6 +366,7 @@ class World {
this.tileSize = tileSize this.tileSize = tileSize
this.tileMaps = tileMaps this.tileMaps = tileMaps
this.chunks = [] this.chunks = []
this.layers = []
this.height = height this.height = height
this.width = width this.width = width
@ -376,6 +378,9 @@ class World {
this._lastDrawCount = 0 this._lastDrawCount = 0
this._lastUpdateCount = 0 this._lastUpdateCount = 0
this._active = [] this._active = []
// Create world layers
this.layers.push(new EntityLayer('ents'))
} }
getChunk (x, y) { getChunk (x, y) {
@ -386,6 +391,14 @@ class World {
return null 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) { update (dt, vp) {
this._active = [] this._active = []
let posPoint = vp.chunkIn(this.chunkSize * this.tileSize) let posPoint = vp.chunkIn(this.chunkSize * this.tileSize)
@ -403,6 +416,7 @@ class World {
} }
} }
// Update chunks
for (let i in this.chunks) { for (let i in this.chunks) {
this.chunks[i].update(dt) this.chunks[i].update(dt)
} }
@ -416,22 +430,26 @@ class World {
let chunk = this.chunks[i] let chunk = this.chunks[i]
let pos = chunk.absPos let pos = chunk.absPos
let distance = distanceTo(vp.adjustCentered, pos) let distance = distanceTo(vp.adjustCentered, pos)
if (distance <= chunk.fullSize * UPDATE_RADIUS) { if (distance <= chunk.fullSize * UPDATE_RADIUS || chunk.modified) {
// Keep chunk // Keep chunk
keep.push(chunk) keep.push(chunk)
} }
} }
this.chunks = keep this.chunks = keep
} }
// Update layers
for (let i in this.layers) {
this.layers[i].update(dt, vp, this)
}
} }
pickMouse (vp, mousePos) { gridPosition (pos) {
let a = this.chunkSize * this.tileSize let a = this.chunkSize * this.tileSize
let abs = { x: mousePos.x + vp.x, y: mousePos.y + vp.y } let chunk = { x: Math.floor(pos.x / a), y: Math.floor(pos.y / a) }
let chunk = { x: Math.floor(abs.x / a), y: Math.floor(abs.y / a) }
let tile = { let tile = {
x: Math.floor(abs.x / this.tileSize - chunk.x * this.chunkSize), x: Math.floor(pos.x / this.tileSize - chunk.x * this.chunkSize),
y: Math.floor(abs.y / this.tileSize - chunk.y * this.chunkSize) y: Math.floor(pos.y / this.tileSize - chunk.y * this.chunkSize)
} }
return { chunk: this.getChunk(chunk.x, chunk.y), tile } return { chunk: this.getChunk(chunk.x, chunk.y), tile }
} }
@ -449,6 +467,11 @@ class World {
if (chunk._updated) this._lastUpdateCount++ if (chunk._updated) this._lastUpdateCount++
this._lastDrawCount++ this._lastDrawCount++
} }
// Draw layers
for (let i in this.layers) {
this.layers[i].draw(vp, this)
}
} }
collide (obj) { collide (obj) {