diff --git a/server/AABB.ts b/server/AABB.ts index 00cf3b3..7d37359 100644 --- a/server/AABB.ts +++ b/server/AABB.ts @@ -7,11 +7,15 @@ export default class AABB { private static readonly aabbPool:FunkyArray = new FunkyArray(); public readonly aabbPoolString:string; + public readonly pooled:boolean; + + public initMin:Vec3; + public initMax:Vec3; public min:Vec3; public max:Vec3; - public constructor(minXOrMin:Vec3 | number, minYOrMax:Vec3 | number, minZ?:number, maxX?:number, maxY?:number, maxZ?:number) { + public constructor(minXOrMin:Vec3 | number, minYOrMax:Vec3 | number, minZ?:number, maxX?:number, maxY?:number, maxZ?:number, pooled:boolean = false) { if (minXOrMin instanceof Vec3 && minYOrMax instanceof Vec3) { this.min = minXOrMin; this.max = minYOrMax; @@ -21,6 +25,11 @@ export default class AABB { } else { throw new Error("Invalid input parameters: AABB must be supplied with either two Vec3 with the min and max bounds or the raw bounds."); } + + this.initMin = new Vec3(this.min); + this.initMax = new Vec3(this.max); + + this.pooled = pooled; this.aabbPoolString = AABB.createAABBPoolString(this.min.x, this.min.y, this.min.z, this.max.x, this.max.y, this.max.z); if (!AABB.aabbPool.has(this.aabbPoolString)) { @@ -36,18 +45,53 @@ export default class AABB { public static getAABB(minX:number, minY:number, minZ:number, maxX:number, maxY:number, maxZ:number) { const aabbPoolString = this.createAABBPoolString(minX, minY, minZ, maxX, maxY, maxZ); - if (!AABB.aabbPool.has(aabbPoolString)) { - return AABB.aabbPool.get(aabbPoolString); + if (AABB.aabbPool.has(aabbPoolString)) { + const aabb = AABB.aabbPool.get(aabbPoolString); + if (aabb === undefined) { + throw new Error(`Pooled AABB was ${typeof(aabb)}! This should be impossible.`); + } + + return aabb; } return new AABB(minX, minY, minZ, maxX, maxY, maxZ); } - intersects(a:AABB, b:AABB) { + public static intersects(a:AABB, b:AABB) { return a.min.x <= b.max.x && a.max.x >= b.min.x && a.min.y <= b.max.y && a.max.y >= b.min.y && a.min.z <= b.max.z && a.max.z >= b.min.z; } - intersectionAmount(a:AABB, b:AABB) { + public intersects(aabb:AABB) { + return this.min.x <= aabb.max.x && this.max.x >= aabb.min.x && this.min.y <= aabb.max.y && this.max.y >= aabb.min.y && this.min.z <= aabb.max.z && this.max.z >= aabb.min.z; + } + public static intersectionY(a: AABB, b: AABB) { + const minY = Math.max(a.min.y, b.min.y); + const maxY = Math.min(a.max.y, b.max.y); + + return minY <= maxY ? maxY - minY : 0; + } + + public intersectionY(aabb: AABB) { + const minY = Math.max(this.min.y, aabb.min.y); + const maxY = Math.min(this.max.y, aabb.max.y); + + return minY <= maxY ? maxY - minY : 0; + } + + public move(xOrVec3:Vec3 | number, y?:number, z?:number) { + if (this.pooled) { + throw new Error(`Attempted to move a pooled AABB. This is not allowed!`); + } + + this.min.set(this.initMin); + this.max.set(this.initMax); + if (xOrVec3 instanceof Vec3) { + this.min.add(xOrVec3); + this.max.add(xOrVec3); + } else if (typeof(xOrVec3) === "number" && typeof(y) === "number" && typeof(z) === "number") { + this.min.add(xOrVec3, y, z); + this.max.add(xOrVec3, y, z); + } } } \ No newline at end of file diff --git a/server/MPClient.ts b/server/MPClient.ts index a8425b8..92e8ca7 100644 --- a/server/MPClient.ts +++ b/server/MPClient.ts @@ -22,6 +22,7 @@ import { PacketDisconnectKick } from "./packets/DisconnectKick"; import { ItemStack } from "./inventories/ItemStack"; import { PacketWindowItems } from "./packets/WindowItems"; import { Block } from "./blocks/Block"; +import { EntityItem } from "./entities/EntityItem"; export class MPClient { private readonly mcServer:MinecraftServer; @@ -160,6 +161,9 @@ export class MPClient { this.entity.world.setBlockWithNotify(this.diggingAt.x, this.diggingAt.y, this.diggingAt.z, 0); this.inventory.addItemStack(new ItemStack(Block.blockBehaviours[brokenBlockId].droppedItem(brokenBlockId), 1, metadata)); this.send(new PacketWindowItems(0, this.inventory.getInventorySize(), this.inventory.constructInventoryPayload()).writeData()); + /*const itemEntity = new EntityItem(this.entity.world, new ItemStack(Block.blockBehaviours[brokenBlockId].droppedItem(brokenBlockId), 1, metadata)); + itemEntity.position.set(x + 0.5, y + 0.5, z + 0.5); + this.entity.world.addEntity(itemEntity);*/ } // TODO: Cap how far away a player is able to break blocks diff --git a/server/Vec3.ts b/server/Vec3.ts index 9ccb132..76af4c3 100644 --- a/server/Vec3.ts +++ b/server/Vec3.ts @@ -35,7 +35,27 @@ export default class Vec3 { this.y = y; this.z = z; } else { - this.x = this.y = this.z = 0; + throw new Error(`Invalid arguments for Vec3.set : ${typeof(x)}, ${typeof(y)}, ${typeof(z)}`); + } + } + + add(x:Vec3 | number, y?:number, z?:number) { + if (x instanceof Vec3) { + this.set(this.x + x.x, this.y + x.y, this.z + x.z); + } else if (typeof(x) === "number" && typeof(y) === "number" && typeof(z) === "number") { + this.set(this.x + x, this.y + y, this.z + z); + } else { + throw new Error(`Invalid arguments for Vec3.add : ${typeof(x)}, ${typeof(y)}, ${typeof(z)}`); + } + } + + mult(x:Vec3 | number, y?:number, z?:number) { + if (x instanceof Vec3) { + this.set(this.x * x.x, this.y * x.y, this.z * x.z); + } else if (typeof(x) === "number" && typeof(y) === "number" && typeof(z) === "number") { + this.set(this.x * x, this.y * y, this.z * z); + } else { + throw new Error(`Invalid arguments for Vec3.mult : ${typeof(x)}, ${typeof(y)}, ${typeof(z)}`); } } } \ No newline at end of file diff --git a/server/World.ts b/server/World.ts index cda91e6..d0497c1 100644 --- a/server/World.ts +++ b/server/World.ts @@ -2,6 +2,7 @@ import { FunkyArray } from "../funkyArray"; import { Chunk } from "./Chunk"; import { WorldSaveManager } from "./WorldSaveManager"; import { Block } from "./blocks/Block"; +import { EntityItem } from "./entities/EntityItem"; import { IEntity } from "./entities/IEntity"; import { Player } from "./entities/Player"; //import { FlatGenerator } from "./generators/Flat"; @@ -9,6 +10,7 @@ import { HillyGenerator } from "./generators/Hilly"; import { IGenerator } from "./generators/IGenerator"; import { PacketBlockChange } from "./packets/BlockChange"; import { PacketDestroyEntity } from "./packets/DestroyEntity"; +import { PacketPickupSpawn } from "./packets/PickupSpawn"; import { QueuedBlockUpdate } from "./queuedUpdateTypes/BlockUpdate"; import { IQueuedUpdate } from "./queuedUpdateTypes/IQueuedUpdate"; @@ -46,6 +48,9 @@ export class World { this.entites.set(entity.entityId, entity); if (entity instanceof Player) { this.players.set(entity.entityId, entity); + } else if (entity instanceof EntityItem) { + const packet = new PacketPickupSpawn(entity.entityId, entity.itemStack.itemID, entity.itemStack.size, entity.itemStack.damage, Math.round(entity.position.x * 32), Math.round(entity.position.y * 32), Math.round(entity.position.z * 32), 0, 0, 0).writeData(); + entity.sendToNearby(packet); } } diff --git a/server/blocks/Block.ts b/server/blocks/Block.ts index 1682ac9..34787fb 100644 --- a/server/blocks/Block.ts +++ b/server/blocks/Block.ts @@ -1,3 +1,4 @@ +import AABB from "../AABB"; import { World } from "../World"; import { BlockBehaviour } from "./BlockBehaviour"; import { BlockBehaviourFlower } from "./BlockBehaviourFlower"; @@ -20,6 +21,7 @@ export class Block { public static readonly blocks:Array = new Array(); public static readonly lightPassage:Array = new Array(); public static readonly hardness:Array = new Array(); + public static readonly blockAABBs:Array = new Array(); public static readonly blockBehaviours:Array = new Array(); public static readonly blockNames:Array = new Array(); @@ -47,6 +49,14 @@ export class Block { Block.hardness[this.blockId] = value; } + private get blockAABB() { + return Block.blockAABBs[this.blockId]; + } + + private set blockAABB(value:AABB) { + Block.blockAABBs[this.blockId] = value; + } + public get blockName() { return Block.blockNames[this.blockId]; } @@ -107,6 +117,10 @@ export class Block { // TODO: Have the 1 be based on current tool ig return 1 / this.hardness / 100; } + + public getBoundingBox(x:number, y:number, z:number) { + return this.behaviour.getBoundingBox(x, y, z); + } // Define statics here static readonly stone = new Block(1).setHardness(1.5).setBehaviour(Behaviour.stone).setBlockName("Stone"); diff --git a/server/blocks/BlockBehaviour.ts b/server/blocks/BlockBehaviour.ts index c4ff9fa..c492e4e 100644 --- a/server/blocks/BlockBehaviour.ts +++ b/server/blocks/BlockBehaviour.ts @@ -1,7 +1,9 @@ +import AABB from "../AABB"; import { World } from "../World"; import { IBlockBehaviour } from "./IBlockBehaviour"; export class BlockBehaviour implements IBlockBehaviour { public neighborBlockChange(world:World, x:number, y:number, z:number, blockId:number) {} public droppedItem(blockId:number) { return blockId; } + public getBoundingBox(x:number, y:number, z:number) { return AABB.getAABB(0 + x, 0 + y, 0 + z, 1 + x, 1 + y, 1 + z); } } \ No newline at end of file diff --git a/server/blocks/BlockBehaviourFlower.ts b/server/blocks/BlockBehaviourFlower.ts index eb1aa66..8418405 100644 --- a/server/blocks/BlockBehaviourFlower.ts +++ b/server/blocks/BlockBehaviourFlower.ts @@ -1,3 +1,4 @@ +import AABB from "../AABB"; import { World } from "../World"; import { Block } from "./Block"; import { BlockBehaviour } from "./BlockBehaviour"; @@ -9,4 +10,8 @@ export class BlockBehaviourFlower extends BlockBehaviour { world.setBlockWithNotify(x, y, z, 0); } } + + public getBoundingBox() { + return AABB.getAABB(0, 0, 0, 0, 0, 0); + } } \ No newline at end of file diff --git a/server/blocks/IBlockBehaviour.ts b/server/blocks/IBlockBehaviour.ts index 56cce8f..81915ff 100644 --- a/server/blocks/IBlockBehaviour.ts +++ b/server/blocks/IBlockBehaviour.ts @@ -1,6 +1,8 @@ +import AABB from "../AABB"; import { World } from "../World"; export interface IBlockBehaviour { neighborBlockChange(world:World, x:number, y:number, z:number, blockId:number): void, - droppedItem: (blockId:number) => number + droppedItem: (blockId:number) => number, + getBoundingBox: (x:number, y:number, z:number) => AABB, } \ No newline at end of file diff --git a/server/entities/Entity.ts b/server/entities/Entity.ts index 43dfc65..a3beae4 100644 --- a/server/entities/Entity.ts +++ b/server/entities/Entity.ts @@ -1,9 +1,11 @@ +import AABB from "../AABB"; import { Chunk } from "../Chunk"; import { MetadataEntry, MetadataWriter } from "../MetadataWriter"; import { Rotation } from "../Rotation"; import { Vec2 } from "../Vec2"; import Vec3 from "../Vec3"; import { World } from "../World"; +import { Block } from "../blocks/Block"; import { MetadataFieldType } from "../enums/MetadataFieldType"; import { PacketEntityLook } from "../packets/EntityLook"; import { PacketEntityLookRelativeMove } from "../packets/EntityLookRelativeMove"; @@ -28,6 +30,8 @@ export class Entity implements IEntity { public lastAbsPosition:Vec3; public motion:Vec3; + private positionBeforeMove:Vec3; + public rotation:Rotation; public lastRotation:Rotation; public absRotation:Rotation; @@ -50,12 +54,18 @@ export class Entity implements IEntity { private lastCrouchState:boolean; private lastFireState:boolean; + public entityAABB:AABB; + + private readonly isPlayer:boolean; private queuedChunkUpdate:boolean; - public constructor(world:World) { + public constructor(world:World, isPlayer:boolean = false) { this.entityId = Entity.nextEntityId++; + this.isPlayer = isPlayer; + this.entitySize = new Vec2(0.6, 1.8); + this.entityAABB = new AABB(-this.entitySize.x / 2, 0, -this.entitySize.x / 2, this.entitySize.x / 2, this.entitySize.y, this.entitySize.x / 2); this.fire = this.fallDistance = 0; this.onGround = false; @@ -68,6 +78,8 @@ export class Entity implements IEntity { this.lastAbsPosition = new Vec3(); this.motion = new Vec3(); + this.positionBeforeMove = new Vec3(); + this.rotation = new Rotation(); this.lastRotation = new Rotation(); this.absRotation = new Rotation(); @@ -201,6 +213,25 @@ export class Entity implements IEntity { } } + moveEntity(motionX:number, motionY:number, motionZ:number) { + this.positionBeforeMove.set(this.position); + const blockId = this.chunk.getBlockId(Math.floor(this.positionBeforeMove.x) & 0xf, Math.floor(this.positionBeforeMove.y), Math.floor(this.positionBeforeMove.z) & 0xf); + const blockUnderEntity = blockId > 0 ? Block.blocks[blockId] : null; + + this.position.add(motionX, motionY, motionZ); + + this.entityAABB.move(this.position); + if (blockUnderEntity !== null) { + const blockBoundingBox = blockUnderEntity.getBoundingBox(Math.floor(this.positionBeforeMove.x), Math.floor(this.positionBeforeMove.y), Math.floor(this.positionBeforeMove.z)); + if (this.entityAABB.intersects(blockBoundingBox)) { + const intersection = this.entityAABB.intersectionY(blockBoundingBox); + this.position.add(0, intersection, 0); + this.motion.y = 0; + this.onGround = true; + } + } + } + onTick() { this.updateMetadata(); this.updateEntityChunk(); diff --git a/server/entities/EntityItem.ts b/server/entities/EntityItem.ts index 413bec4..f2cf04b 100644 --- a/server/entities/EntityItem.ts +++ b/server/entities/EntityItem.ts @@ -6,12 +6,47 @@ export class EntityItem extends Entity { public age:number; public itemStack:ItemStack; + public pickupDelay:number; + public constructor(world:World, itemStack:ItemStack) { super(world); this.itemStack = itemStack; + this.entitySize.set(0.2, 0.2); + + this.pickupDelay = 0; + + this.motion.set(Math.random() * 0.2 - 0.1, 0.2, Math.random() * 0.2 - 0.1); + this.age = 0; this.health = 5; } + + onTick() { + super.onTick(); + if (this.pickupDelay > 0) { + this.pickupDelay--; + } + + this.motion.add(0, -0.04, 0); + this.moveEntity(this.motion.x, this.motion.y, this.motion.z); + + let xyMult = 0.98; + if (this.onGround) { + xyMult = 0.59; + } + + // TODO: Change the x and z based on the slipperiness of a block + this.motion.mult(xyMult, 0.98, xyMult); + + if (this.onGround) { + this.motion.y *= -0.5; + } + + this.age++; + if (this.age >= 6000) { + // TODO: Kill entity + } + } } \ No newline at end of file diff --git a/server/entities/EntityLiving.ts b/server/entities/EntityLiving.ts index cdd92e8..6ffbb3a 100644 --- a/server/entities/EntityLiving.ts +++ b/server/entities/EntityLiving.ts @@ -18,8 +18,8 @@ export class EntityLiving extends Entity { public headHeight:number; public lastHealth:number; - public constructor(world:World) { - super(world); + public constructor(world:World, isPlayer:boolean = false) { + super(world, isPlayer); this.timeInWater = 0; this.headHeight = 1.62; diff --git a/server/entities/Player.ts b/server/entities/Player.ts index 4cf1f29..b5e9a3c 100644 --- a/server/entities/Player.ts +++ b/server/entities/Player.ts @@ -27,7 +27,7 @@ export class Player extends EntityLiving { public trackedEquipment:Array; public constructor(server:MinecraftServer, world:World, username:string) { - super(world); + super(world, true); this.server = server; this.firstUpdate = true; this.loadedChunks = new Array(); @@ -140,6 +140,9 @@ export class Player extends EntityLiving { // Calculate player motion since we don't have it serverside. this.motion.set(this.position.x - this.lastPosition.x, this.position.y - this.lastPosition.y, this.position.z - this.lastPosition.z); + if (!this.motion.isZero) { + this.entityAABB.move(this.position); + } super.onTick(); diff --git a/server/enums/Packet.ts b/server/enums/Packet.ts index a743e1d..3de4911 100644 --- a/server/enums/Packet.ts +++ b/server/enums/Packet.ts @@ -21,6 +21,7 @@ export enum Packet { Animation = 0x12, EntityAction = 0x13, NamedEntitySpawn = 0x14, + PickupSpawn = 0x15, EntityVelocity = 0x1C, DestroyEntity = 0x1D, diff --git a/server/packets/PickupSpawn.ts b/server/packets/PickupSpawn.ts new file mode 100644 index 0000000..d9fa560 --- /dev/null +++ b/server/packets/PickupSpawn.ts @@ -0,0 +1,62 @@ +import { createWriter, IReader, Endian } from "bufferstuff"; +import { IPacket } from "./IPacket"; +import { Packet } from "../enums/Packet"; + +export class PacketPickupSpawn implements IPacket { + public packetId = Packet.PickupSpawn; + public entityId:number; + public item:number; + public count:number; + public damage:number; + public x:number; + public y:number; + public z:number; + public yaw:number; + public pitch:number; + public roll:number; + + public constructor(entityId?:number, item?:number, count?:number, damage?:number, x?:number, y?:number, z?:number, yaw?:number, pitch?:number, roll?:number) { + if (typeof(entityId) === "number" && typeof(item) === "number" && typeof(count) === "number" && typeof(damage) === "number" && typeof(x) === "number" && typeof(y) === "number" && typeof(z) === "number" && typeof(yaw) === "number" && typeof(pitch) === "number" && typeof(roll) === "number") { + this.entityId = entityId; + this.item = item; + this.count = count; + this.damage = damage; + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + this.pitch = pitch; + this.roll = roll; + } else { + this.entityId = Number.MIN_VALUE; + this.item = Number.MIN_VALUE; + this.count = Number.MIN_VALUE; + this.damage = Number.MIN_VALUE; + this.x = Number.MIN_VALUE; + this.y = Number.MIN_VALUE; + this.z = Number.MIN_VALUE; + this.yaw = Number.MIN_VALUE; + this.pitch = Number.MIN_VALUE; + this.roll = Number.MIN_VALUE; + } + } + + public readData(reader:IReader) { + this.entityId = reader.readInt(); + this.item = reader.readShort(); + this.count = reader.readByte(); + this.damage = reader.readShort(); + this.x = reader.readInt(); + this.y = reader.readInt(); + this.z = reader.readInt(); + this.yaw = reader.readByte(); + this.pitch = reader.readByte(); + this.roll = reader.readByte(); + + return this; + } + + public writeData() { + return createWriter(Endian.BE, 25).writeUByte(this.packetId).writeInt(this.entityId).writeShort(this.item).writeByte(this.count).writeShort(this.damage).writeInt(this.x).writeInt(this.y).writeInt(this.z).writeByte(this.yaw).writeByte(this.pitch).writeByte(this.roll).toBuffer(); + } +} \ No newline at end of file