diff --git a/src/pocketmine/entity/Entity.php b/src/pocketmine/entity/Entity.php index c64bcd2a3..aeb2c76e5 100644 --- a/src/pocketmine/entity/Entity.php +++ b/src/pocketmine/entity/Entity.php @@ -355,7 +355,7 @@ abstract class Entity extends Location implements Metadatable, EntityIds{ protected $lastDamageCause = null; /** @var Block[] */ - private $blocksAround = []; + protected $blocksAround = []; /** @var float|null */ public $lastX = null; @@ -1387,7 +1387,7 @@ abstract class Entity extends Location implements Metadatable, EntityIds{ * Returns whether the entity needs a movement update on the next tick. * @return bool */ - final public function hasMovementUpdate() : bool{ + public function hasMovementUpdate() : bool{ return ( $this->forceMovementUpdate or $this->motionX != 0 or diff --git a/src/pocketmine/entity/projectile/Arrow.php b/src/pocketmine/entity/projectile/Arrow.php index 8757690a5..d30d98182 100644 --- a/src/pocketmine/entity/projectile/Arrow.php +++ b/src/pocketmine/entity/projectile/Arrow.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\entity\projectile; use pocketmine\entity\Entity; +use pocketmine\event\entity\ProjectileHitEvent; use pocketmine\event\inventory\InventoryPickupArrowEvent; use pocketmine\item\Item; use pocketmine\item\ItemFactory; @@ -72,10 +73,6 @@ class Arrow extends Projectile{ $hasUpdate = parent::entityBaseTick($tickDiff); - if($this->onGround or $this->hadCollision){ - $this->setCritical(false); - } - if($this->age > 1200){ $this->flagForDespawn(); $hasUpdate = true; @@ -84,8 +81,12 @@ class Arrow extends Projectile{ return $hasUpdate; } + protected function onHit(ProjectileHitEvent $event) : void{ + $this->setCritical(false); + } + public function onCollideWithPlayer(Player $player){ - if(!$this->hadCollision){ + if($this->blockHit === null){ return; } diff --git a/src/pocketmine/entity/projectile/Projectile.php b/src/pocketmine/entity/projectile/Projectile.php index d446c96cd..3f503415b 100644 --- a/src/pocketmine/entity/projectile/Projectile.php +++ b/src/pocketmine/entity/projectile/Projectile.php @@ -23,16 +23,24 @@ declare(strict_types=1); namespace pocketmine\entity\projectile; +use pocketmine\block\Block; use pocketmine\entity\Entity; use pocketmine\entity\Living; use pocketmine\event\entity\EntityCombustByEntityEvent; use pocketmine\event\entity\EntityDamageByChildEntityEvent; use pocketmine\event\entity\EntityDamageByEntityEvent; use pocketmine\event\entity\EntityDamageEvent; +use pocketmine\event\entity\ProjectileHitBlockEvent; +use pocketmine\event\entity\ProjectileHitEntityEvent; use pocketmine\event\entity\ProjectileHitEvent; +use pocketmine\event\Timings; use pocketmine\level\Level; +use pocketmine\math\RayTraceResult; use pocketmine\math\Vector3; +use pocketmine\math\VoxelRayTrace; +use pocketmine\nbt\tag\ByteTag; use pocketmine\nbt\tag\CompoundTag; +use pocketmine\nbt\tag\IntTag; abstract class Projectile extends Entity{ @@ -40,7 +48,12 @@ abstract class Projectile extends Entity{ protected $damage = 0; - public $hadCollision = false; + /** @var Vector3|null */ + protected $blockHit; + /** @var int|null */ + protected $blockHitId; + /** @var int|null */ + protected $blockHitData; public function __construct(Level $level, CompoundTag $nbt, Entity $shootingEntity = null){ parent::__construct($level, $nbt); @@ -61,6 +74,34 @@ abstract class Projectile extends Entity{ $this->setMaxHealth(1); $this->setHealth(1); $this->age = $this->namedtag->getShort("Age", $this->age); + + do{ + $blockHit = null; + $blockId = null; + $blockData = null; + + if($this->namedtag->hasTag("tileX", IntTag::class) and $this->namedtag->hasTag("tileY", IntTag::class) and $this->namedtag->hasTag("tileZ", IntTag::class)){ + $blockHit = new Vector3($this->namedtag->getInt("tileX"), $this->namedtag->getInt("tileY"), $this->namedtag->getInt("tileZ")); + }else{ + break; + } + + if($this->namedtag->hasTag("blockId", ByteTag::class)){ + $blockId = $this->namedtag->getByte("blockId"); + }else{ + break; + } + + if($this->namedtag->hasTag("blockData", ByteTag::class)){ + $blockData = $this->namedtag->getByte("blockData"); + }else{ + break; + } + + $this->blockHit = $blockHit; + $this->blockHitId = $blockId; + $this->blockHitData = $blockData; + }while(false); } public function canCollideWith(Entity $entity) : bool{ @@ -79,107 +120,198 @@ abstract class Projectile extends Entity{ return (int) ceil(sqrt($this->motionX ** 2 + $this->motionY ** 2 + $this->motionZ ** 2) * $this->damage); } - public function onCollideWithEntity(Entity $entity){ - $this->server->getPluginManager()->callEvent(new ProjectileHitEvent($this)); - - $damage = $this->getResultDamage(); - - if($this->getOwningEntity() === null){ - $ev = new EntityDamageByEntityEvent($this, $entity, EntityDamageEvent::CAUSE_PROJECTILE, $damage); - }else{ - $ev = new EntityDamageByChildEntityEvent($this->getOwningEntity(), $this, $entity, EntityDamageEvent::CAUSE_PROJECTILE, $damage); - } - - $entity->attack($ev); - - $this->hadCollision = true; - - if($this->fireTicks > 0){ - $ev = new EntityCombustByEntityEvent($this, $entity, 5); - $this->server->getPluginManager()->callEvent($ev); - if(!$ev->isCancelled()){ - $entity->setOnFire($ev->getDuration()); - } - } - - $this->flagForDespawn(); - } - public function saveNBT(){ parent::saveNBT(); + $this->namedtag->setShort("Age", $this->age); + + if($this->blockHit !== null){ + $this->namedtag->setInt("tileX", $this->blockHit->x); + $this->namedtag->setInt("tileY", $this->blockHit->y); + $this->namedtag->setInt("tileZ", $this->blockHit->z); + + //we intentionally use different ones to PC because we don't have stringy IDs + $this->namedtag->setByte("blockId", $this->blockHitId); + $this->namedtag->setByte("blockData", $this->blockHitData); + } } protected function applyDragBeforeGravity() : bool{ return true; } - public function entityBaseTick(int $tickDiff = 1) : bool{ - if($this->closed){ - return false; - } + public function hasMovementUpdate() : bool{ + $parent = parent::hasMovementUpdate(); + if($parent and $this->blockHit !== null){ + $blockIn = $this->level->getBlockAt($this->blockHit->x, $this->blockHit->y, $this->blockHit->z); - $hasUpdate = parent::entityBaseTick($tickDiff); - - if(!$this->isFlaggedForDespawn()){ - $movingObjectPosition = null; - - $moveVector = new Vector3($this->x + $this->motionX, $this->y + $this->motionY, $this->z + $this->motionZ); - - $list = $this->getLevel()->getCollidingEntities($this->boundingBox->addCoord($this->motionX, $this->motionY, $this->motionZ)->expand(1, 1, 1), $this); - - $nearDistance = PHP_INT_MAX; - $nearEntity = null; - - foreach($list as $entity){ - if(/*!$entity->canCollideWith($this) or */ - ($entity->getId() === $this->getOwningEntityId() and $this->ticksLived < 5) - ){ - continue; - } - - $axisalignedbb = $entity->boundingBox->grow(0.3, 0.3, 0.3); - $rayTraceResult = $axisalignedbb->calculateIntercept($this, $moveVector); - - if($rayTraceResult === null){ - continue; - } - - $distance = $this->distanceSquared($rayTraceResult->hitVector); - - if($distance < $nearDistance){ - $nearDistance = $distance; - $nearEntity = $entity; - } - } - - if($nearEntity !== null){ - $this->onCollideWithEntity($nearEntity); + if($blockIn->getId() === $this->blockHitId and $blockIn->getDamage() === $this->blockHitData){ return false; } - - if($this->isCollided and !$this->hadCollision){ //Collided with a block - $this->hadCollision = true; - - $this->motionX = 0; - $this->motionY = 0; - $this->motionZ = 0; - - $this->server->getPluginManager()->callEvent(new ProjectileHitEvent($this)); - return false; - }elseif(!$this->isCollided and $this->hadCollision){ //Previously collided with block, but block later removed - $this->hadCollision = false; - } - - if(!$this->hadCollision or abs($this->motionX) > self::MOTION_THRESHOLD or abs($this->motionY) > self::MOTION_THRESHOLD or abs($this->motionZ) > self::MOTION_THRESHOLD){ - $f = sqrt(($this->motionX ** 2) + ($this->motionZ ** 2)); - $this->yaw = (atan2($this->motionX, $this->motionZ) * 180 / M_PI); - $this->pitch = (atan2($this->motionY, $f) * 180 / M_PI); - $hasUpdate = true; - } } - return $hasUpdate; + return $parent; } + public function move(float $dx, float $dy, float $dz) : bool{ + $this->blocksAround = null; + + Timings::$entityMoveTimer->startTiming(); + + $start = $this->asVector3(); + $end = $start->add($this->motionX, $this->motionY, $this->motionZ); + + $blockHit = null; + $entityHit = null; + $hitResult = null; + + foreach(VoxelRayTrace::betweenPoints($start, $end) as $vector3){ + $block = $this->level->getBlockAt($vector3->x, $vector3->y, $vector3->z); + + $blockHitResult = $this->calculateInterceptWithBlock($block, $start, $end); + if($blockHitResult !== null){ + $end = $blockHitResult->hitVector; + $blockHit = $block; + $hitResult = $blockHitResult; + break; + } + } + + $entityDistance = PHP_INT_MAX; + + $newDiff = $end->subtract($start); + foreach($this->level->getCollidingEntities($this->boundingBox->addCoord($newDiff->x, $newDiff->y, $newDiff->z)->expand(1, 1, 1), $this) as $entity){ + if($entity->getId() === $this->getOwningEntityId() and $this->ticksLived < 5){ + continue; + } + + $entityBB = $entity->boundingBox->grow(0.3, 0.3, 0.3); + $entityHitResult = $entityBB->calculateIntercept($start, $end); + + if($entityHitResult === null){ + continue; + } + + $distance = $this->distanceSquared($entityHitResult->hitVector); + + if($distance < $entityDistance){ + $entityDistance = $distance; + $entityHit = $entity; + $hitResult = $entityHitResult; + $end = $entityHitResult->hitVector; + } + } + + $this->x = $end->x; + $this->y = $end->y; + $this->z = $end->z; + $this->recalculateBoundingBox(); + + if($hitResult !== null){ + /** @var ProjectileHitEvent|null $ev */ + $ev = null; + if($entityHit !== null){ + $ev = new ProjectileHitEntityEvent($this, $hitResult, $entityHit); + }elseif($blockHit !== null){ + $ev = new ProjectileHitBlockEvent($this, $hitResult, $blockHit); + }else{ + \assert(false, "unknown hit type"); + } + + if($ev !== null){ + $this->server->getPluginManager()->callEvent($ev); + $this->onHit($ev); + + if($ev instanceof ProjectileHitEntityEvent){ + $this->onHitEntity($ev->getEntityHit(), $ev->getRayTraceResult()); + }elseif($ev instanceof ProjectileHitBlockEvent){ + $this->onHitBlock($ev->getBlockHit(), $ev->getRayTraceResult()); + } + } + + $this->isCollided = $this->onGround = true; + $this->motionX = $this->motionY = $this->motionZ = 0; + }else{ + $this->isCollided = $this->onGround = false; + $this->blockHit = $this->blockHitId = $this->blockHitData = null; + + //recompute angles... + $f = sqrt(($this->motionX ** 2) + ($this->motionZ ** 2)); + $this->yaw = (atan2($this->motionX, $this->motionZ) * 180 / M_PI); + $this->pitch = (atan2($this->motionY, $f) * 180 / M_PI); + } + + $this->checkChunks(); + $this->checkBlockCollision(); + + + Timings::$entityMoveTimer->stopTiming(); + + return true; + } + + /** + * Called by move() when raytracing blocks to discover whether the block should be considered as a point of impact. + * This can be overridden by other projectiles to allow altering the blocks which are collided with (for example + * some projectiles collide with any non-air block). + * + * @param Block $block + * @param Vector3 $start + * @param Vector3 $end + * + * @return RayTraceResult|null the result of the ray trace if successful, or null if no interception is found. + */ + protected function calculateInterceptWithBlock(Block $block, Vector3 $start, Vector3 $end) : ?RayTraceResult{ + return $block->calculateIntercept($start, $end); + } + + /** + * Called when the projectile hits something. Override this to perform non-target-specific effects when the + * projectile hits something. + * + * @param ProjectileHitEvent $event + */ + protected function onHit(ProjectileHitEvent $event) : void{ + + } + + /** + * Called when the projectile collides with an Entity. + * + * @param Entity $entityHit + * @param RayTraceResult $hitResult + */ + protected function onHitEntity(Entity $entityHit, RayTraceResult $hitResult) : void{ + $damage = $this->getResultDamage(); + + if($this->getOwningEntity() === null){ + $ev = new EntityDamageByEntityEvent($this, $entityHit, EntityDamageEvent::CAUSE_PROJECTILE, $damage); + }else{ + $ev = new EntityDamageByChildEntityEvent($this->getOwningEntity(), $this, $entityHit, EntityDamageEvent::CAUSE_PROJECTILE, $damage); + } + + $entityHit->attack($ev); + + if($this->fireTicks > 0){ + $ev = new EntityCombustByEntityEvent($this, $entityHit, 5); + $this->server->getPluginManager()->callEvent($ev); + if(!$ev->isCancelled()){ + $entityHit->setOnFire($ev->getDuration()); + } + } + + $this->flagForDespawn(); + } + + /** + * Called when the projectile collides with a Block. + * + * @param Block $blockHit + * @param RayTraceResult $hitResult + */ + protected function onHitBlock(Block $blockHit, RayTraceResult $hitResult) : void{ + $this->blockHit = $blockHit->asVector3(); + $this->blockHitId = $blockHit->getId(); + $this->blockHitData = $blockHit->getDamage(); + } } diff --git a/src/pocketmine/entity/projectile/Throwable.php b/src/pocketmine/entity/projectile/Throwable.php index 8770dd23d..9eef34780 100644 --- a/src/pocketmine/entity/projectile/Throwable.php +++ b/src/pocketmine/entity/projectile/Throwable.php @@ -39,7 +39,6 @@ abstract class Throwable extends Projectile{ $hasUpdate = parent::entityBaseTick($tickDiff); if($this->age > 1200 or $this->isCollided){ - //TODO: hit particles $this->flagForDespawn(); $hasUpdate = true; } diff --git a/src/pocketmine/event/entity/ProjectileHitBlockEvent.php b/src/pocketmine/event/entity/ProjectileHitBlockEvent.php new file mode 100644 index 000000000..8fe272ad6 --- /dev/null +++ b/src/pocketmine/event/entity/ProjectileHitBlockEvent.php @@ -0,0 +1,49 @@ +blockHit = $blockHit; + } + + /** + * Returns the Block struck by the projectile. + * Hint: to get the block face hit, look at the RayTraceResult. + * + * @return Block + */ + public function getBlockHit() : Block{ + return $this->blockHit; + } +} diff --git a/src/pocketmine/event/entity/ProjectileHitEntityEvent.php b/src/pocketmine/event/entity/ProjectileHitEntityEvent.php new file mode 100644 index 000000000..527986a77 --- /dev/null +++ b/src/pocketmine/event/entity/ProjectileHitEntityEvent.php @@ -0,0 +1,48 @@ +entityHit = $entityHit; + } + + /** + * Returns the Entity struck by the projectile. + * + * @return Entity + */ + public function getEntityHit() : Entity{ + return $this->entityHit; + } +} diff --git a/src/pocketmine/event/entity/ProjectileHitEvent.php b/src/pocketmine/event/entity/ProjectileHitEvent.php index 713a2a2b5..f118628e3 100644 --- a/src/pocketmine/event/entity/ProjectileHitEvent.php +++ b/src/pocketmine/event/entity/ProjectileHitEvent.php @@ -24,16 +24,21 @@ declare(strict_types=1); namespace pocketmine\event\entity; use pocketmine\entity\projectile\Projectile; +use pocketmine\math\RayTraceResult; -class ProjectileHitEvent extends EntityEvent{ +abstract class ProjectileHitEvent extends EntityEvent{ public static $handlerList = null; - /** - * @param Projectile $entity - */ - public function __construct(Projectile $entity){ - $this->entity = $entity; + /** @var RayTraceResult */ + private $rayTraceResult; + /** + * @param Projectile $entity + * @param RayTraceResult $rayTraceResult + */ + public function __construct(Projectile $entity, RayTraceResult $rayTraceResult){ + $this->entity = $entity; + $this->rayTraceResult = $rayTraceResult; } /** @@ -43,4 +48,13 @@ class ProjectileHitEvent extends EntityEvent{ return $this->entity; } + /** + * Returns a RayTraceResult object containing information such as the exact position struck, the AABB it hit, and + * the face of the AABB that it hit. + * + * @return RayTraceResult + */ + public function getRayTraceResult() : RayTraceResult{ + return $this->rayTraceResult; + } }