319 lines
9.3 KiB
PHP

<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\entity\projectile;
use pocketmine\block\Block;
use pocketmine\block\BlockFactory;
use pocketmine\entity\Entity;
use pocketmine\entity\Living;
use pocketmine\entity\Location;
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\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;
use pocketmine\timings\Timings;
use function assert;
use function atan2;
use function ceil;
use function sqrt;
use const M_PI;
use const PHP_INT_MAX;
abstract class Projectile extends Entity{
/** @var float */
protected $damage = 0.0;
/** @var Block|null */
protected $blockHit;
public function __construct(Location $location, ?Entity $shootingEntity, ?CompoundTag $nbt = null){
parent::__construct($location, $nbt);
if($shootingEntity !== null){
$this->setOwningEntity($shootingEntity);
}
}
public function attack(EntityDamageEvent $source) : void{
if($source->getCause() === EntityDamageEvent::CAUSE_VOID){
parent::attack($source);
}
}
protected function initEntity(CompoundTag $nbt) : void{
parent::initEntity($nbt);
$this->setMaxHealth(1);
$this->setHealth(1);
$this->damage = $nbt->getDouble("damage", $this->damage);
(function() use ($nbt) : void{
if(($tileXTag = $nbt->getTag("tileX")) instanceof IntTag and ($tileYTag = $nbt->getTag("tileY")) instanceof IntTag and ($tileZTag = $nbt->getTag("tileZ")) instanceof IntTag){
$blockPos = new Vector3($tileXTag->getValue(), $tileYTag->getValue(), $tileZTag->getValue());
}else{
return;
}
if(($blockIdTag = $nbt->getTag("blockId")) instanceof IntTag){
$blockId = $blockIdTag->getValue();
}else{
return;
}
if(($blockDataTag = $nbt->getTag("blockData")) instanceof ByteTag){
$blockData = $blockDataTag->getValue();
}else{
return;
}
$this->blockHit = BlockFactory::getInstance()->get($blockId, $blockData);
$this->blockHit->position($this->getWorld(), $blockPos->getFloorX(), $blockPos->getFloorY(), $blockPos->getFloorZ());
})();
}
public function canCollideWith(Entity $entity) : bool{
return $entity instanceof Living and !$this->onGround;
}
public function canBeCollidedWith() : bool{
return false;
}
/**
* Returns the base damage applied on collision. This is multiplied by the projectile's speed to give a result
* damage.
*/
public function getBaseDamage() : float{
return $this->damage;
}
/**
* Sets the base amount of damage applied by the projectile.
*/
public function setBaseDamage(float $damage) : void{
$this->damage = $damage;
}
/**
* Returns the amount of damage this projectile will deal to the entity it hits.
*/
public function getResultDamage() : int{
return (int) ceil($this->motion->length() * $this->damage);
}
public function saveNBT() : CompoundTag{
$nbt = parent::saveNBT();
$nbt->setDouble("damage", $this->damage);
if($this->blockHit !== null){
$pos = $this->blockHit->getPosition();
$nbt->setInt("tileX", $pos->x);
$nbt->setInt("tileY", $pos->y);
$nbt->setInt("tileZ", $pos->z);
//we intentionally use different ones to PC because we don't have stringy IDs
$nbt->setInt("blockId", $this->blockHit->getId());
$nbt->setByte("blockData", $this->blockHit->getMeta());
}
return $nbt;
}
protected function applyDragBeforeGravity() : bool{
return true;
}
public function onNearbyBlockChange() : void{
if($this->blockHit !== null and $this->getWorld()->isInLoadedTerrain($this->blockHit->getPosition()) and !$this->blockHit->isSameState($this->getWorld()->getBlock($this->blockHit->getPosition()))){
$this->blockHit = null;
}
parent::onNearbyBlockChange();
}
public function hasMovementUpdate() : bool{
return $this->blockHit === null and parent::hasMovementUpdate();
}
protected function move(float $dx, float $dy, float $dz) : void{
$this->blocksAround = null;
Timings::$entityMove->startTiming();
$start = $this->location->asVector3();
$end = $start->add($dx, $dy, $dz);
$blockHit = null;
$entityHit = null;
$hitResult = null;
foreach(VoxelRayTrace::betweenPoints($start, $end) as $vector3){
$block = $this->getWorld()->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->subtractVector($start);
foreach($this->getWorld()->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->expandedCopy(0.3, 0.3, 0.3);
$entityHitResult = $entityBB->calculateIntercept($start, $end);
if($entityHitResult === null){
continue;
}
$distance = $this->location->distanceSquared($entityHitResult->hitVector);
if($distance < $entityDistance){
$entityDistance = $distance;
$entityHit = $entity;
$hitResult = $entityHitResult;
$end = $entityHitResult->hitVector;
}
}
$this->location = Location::fromObject(
$end,
$this->location->world,
$this->location->yaw,
$this->location->pitch
);
$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){
$ev->call();
$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->motion = new Vector3(0, 0, 0);
}else{
$this->isCollided = $this->onGround = false;
$this->blockHit = null;
//recompute angles...
$f = sqrt(($this->motion->x ** 2) + ($this->motion->z ** 2));
$this->location->yaw = (atan2($this->motion->x, $this->motion->z) * 180 / M_PI);
$this->location->pitch = (atan2($this->motion->y, $f) * 180 / M_PI);
}
$this->getWorld()->onEntityMoved($this);
$this->checkBlockIntersections();
Timings::$entityMove->stopTiming();
}
/**
* 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).
*
* @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.
*/
protected function onHit(ProjectileHitEvent $event) : void{
}
/**
* Called when the projectile collides with an Entity.
*/
protected function onHitEntity(Entity $entityHit, RayTraceResult $hitResult) : void{
$damage = $this->getResultDamage();
if($damage >= 0){
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->isOnFire()){
$ev = new EntityCombustByEntityEvent($this, $entityHit, 5);
$ev->call();
if(!$ev->isCancelled()){
$entityHit->setOnFire($ev->getDuration());
}
}
}
$this->flagForDespawn();
}
/**
* Called when the projectile collides with a Block.
*/
protected function onHitBlock(Block $blockHit, RayTraceResult $hitResult) : void{
$this->blockHit = clone $blockHit;
}
}