From 71eaa328a454dd071130c90937346461df4305f3 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sat, 11 Jan 2020 13:11:34 +0200 Subject: [PATCH] item entities --- src/debug.js | 3 +- src/entity.js | 214 ++++++++++++++++++++++++++++++++++++++++++++++++ src/index.js | 93 +-------------------- src/items.js | 16 +++- src/player.js | 107 ++++++++++++++++-------- src/register.js | 60 ++++++++++++++ src/tiles.js | 35 ++++++-- 7 files changed, 395 insertions(+), 133 deletions(-) create mode 100644 src/entity.js create mode 100644 src/register.js diff --git a/src/debug.js b/src/debug.js index b0a2b67..5624863 100644 --- a/src/debug.js +++ b/src/debug.js @@ -24,7 +24,8 @@ class Debugging { ctx.fillText('active ' + world._active.length, 4, 16 * 7) // Mouse 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) if (mpin.chunk) { ctx.fillText('mouse-in-chunk (x: ' + mpin.chunk.x + '; y: ' + mpin.chunk.y + ')', 4, 16 * 10) diff --git a/src/entity.js b/src/entity.js new file mode 100644 index 0000000..0ddfb44 --- /dev/null +++ b/src/entity.js @@ -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 } diff --git a/src/index.js b/src/index.js index 7007899..ea1b352 100644 --- a/src/index.js +++ b/src/index.js @@ -1,14 +1,14 @@ /* global requestAnimationFrame */ import { canvas, ctx } from './canvas' -import { Tile, TileMap, World } from './tiles' -import { ItemPlaceable } from './items' -import { Inventory } from './inventory' +import { World } from './tiles' +// import { ItemPlaceable } from './items' import { HeightMap } from './heightmap' import Debug from './debug' import Player from './player' import Input from './input' import Viewport from './viewport' import RES from './resource' +import map from './register' let playing = false let frameTime = 0 @@ -17,111 +17,24 @@ 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) 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.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) function update (dt) { world.update(dt, vp) 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) { - 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 () { world.draw(vp) p.draw(vp) Debug.draw(vp, world, fps) - inv.draw() } function step () { diff --git a/src/items.js b/src/items.js index 1dda81c..35c3056 100644 --- a/src/items.js +++ b/src/items.js @@ -36,16 +36,22 @@ class ItemPlaceable extends Item { } } +class ItemTool extends Item { + use (dt, world, player) {} + + useSecondary (dt, world, player) {} +} + class ItemStack { static fromIString (str) { if (typeof str !== 'string') return let strpl = str.split(' ') let iname = strpl[0] - let count = strpl[1] + let count = parseInt(strpl[1]) let item = ItemRegistry.get(iname) let istack = new ItemStack() istack.item = item - istack.count = count || 1 + istack.count = isNaN(count) ? 1 : count return istack } @@ -82,6 +88,10 @@ class ItemStack { a.count = c 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 } diff --git a/src/player.js b/src/player.js index fd41de2..c0bf012 100644 --- a/src/player.js +++ b/src/player.js @@ -1,50 +1,54 @@ import { ctx } from './canvas' +import { PhysicsEntity, ItemEntity } from './entity' +import { ItemStack } from './items' +import { Inventory } from './inventory' import Input from './input' -class Player { +class Player extends 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 + super(x, y, w, h) this.speed = 8 this.gravity = 1 this.jumpPower = 20 + + this.inv = new Inventory(9) + this.itemPickUpDistance = 40 } - moveAndSlide (collider) { - // y collision - 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 - 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 + handleTool (dt, vp, world) { + let mabs = { x: Input.mouse.pos.x + vp.x, y: Input.mouse.pos.y + vp.y } + let mpin = world.gridPosition(mabs) + if (Input.mouse['btn0']) { + if (mpin.chunk) { + if (this.inv.isEmpty(this.inv.selected)) return + let tile = mpin.chunk.getTile('fg', mpin.tile) + if (tile !== -1) return + let itm = this.inv.getItem(this.inv.selected) + if (itm && itm.item.placeable) { + let success = mpin.chunk.setTile('fg', mpin.tile, itm.item.placeable.id) + if (success) { + this.inv.takeItem(this.inv.selected, 1) + } + } + } + } else if (Input.mouse['btn2']) { + if (mpin.chunk) { + let layer = mpin.chunk.getLayer('fg') + let tile = layer.tileAtXY(mpin.tile.x, mpin.tile.y) + 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 if (Input.isDown('a')) { this.mX = -this.speed @@ -64,9 +68,46 @@ class Player { 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) { ctx.fillStyle = '#f00' ctx.fillRect(this.x - vp.x, this.y - vp.y, this.width, this.height) + this.inv.draw() } } diff --git a/src/register.js b/src/register.js new file mode 100644 index 0000000..011d164 --- /dev/null +++ b/src/register.js @@ -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 diff --git a/src/tiles.js b/src/tiles.js index 40d27b4..136e4da 100644 --- a/src/tiles.js +++ b/src/tiles.js @@ -2,6 +2,7 @@ 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 @@ -365,6 +366,7 @@ class World { this.tileSize = tileSize this.tileMaps = tileMaps this.chunks = [] + this.layers = [] this.height = height this.width = width @@ -376,6 +378,9 @@ class World { this._lastDrawCount = 0 this._lastUpdateCount = 0 this._active = [] + + // Create world layers + this.layers.push(new EntityLayer('ents')) } getChunk (x, y) { @@ -386,6 +391,14 @@ class World { 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) @@ -403,6 +416,7 @@ class World { } } + // Update chunks for (let i in this.chunks) { this.chunks[i].update(dt) } @@ -416,22 +430,26 @@ class World { let chunk = this.chunks[i] let pos = chunk.absPos let distance = distanceTo(vp.adjustCentered, pos) - if (distance <= chunk.fullSize * UPDATE_RADIUS) { + 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) + } } - pickMouse (vp, mousePos) { + gridPosition (pos) { let a = this.chunkSize * this.tileSize - let abs = { x: mousePos.x + vp.x, y: mousePos.y + vp.y } - let chunk = { x: Math.floor(abs.x / a), y: Math.floor(abs.y / a) } + let chunk = { x: Math.floor(pos.x / a), y: Math.floor(pos.y / a) } let tile = { - x: Math.floor(abs.x / this.tileSize - chunk.x * this.chunkSize), - y: Math.floor(abs.y / this.tileSize - chunk.y * this.chunkSize) + 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 } } @@ -449,6 +467,11 @@ class World { if (chunk._updated) this._lastUpdateCount++ this._lastDrawCount++ } + + // Draw layers + for (let i in this.layers) { + this.layers[i].draw(vp, this) + } } collide (obj) {